From 741f534eeb4d02371c9238a8f28ca74d006cf8b6 Mon Sep 17 00:00:00 2001 From: cjharriskc-ai Date: Mon, 27 Jul 2026 02:28:16 -0500 Subject: [PATCH] Restore repository to pre-DumbMoney state --- .github/ci/fast_subset.txt | 8 - .github/workflows/tests.yml | 1 - PROTECTED_MANIFEST.json | 23 +- PUBLIC_SOURCE_MANIFEST.json | 2 +- .../dopey-robinhood-runner.v1.template.json | 44 - .../dopey-robinhood-runner.v1.schema.json | 183 -- docs/DUMBMONEY_ROBINHOOD_CELL.md | 437 ---- dopey_live/__init__.py | 30 +- dopey_live/dumbmoney_authority.py | 1172 ----------- dopey_live/dumbmoney_command_feed.py | 1596 --------------- dopey_live/dumbmoney_contract_feed.py | 846 -------- dopey_live/dumbmoney_journal_anchor.py | 595 ------ dopey_live/dumbmoney_promotion.py | 585 ------ dopey_live/dumbmoney_windows_service.py | 1790 ----------------- dopey_live/ledger.py | 117 -- dopey_live/models.py | 29 +- dopey_live/robinhood_gateway.py | 1390 ++----------- dopey_live/robinhood_mcp_direct.py | 512 ----- dopey_live/service.py | 1280 +----------- dopey_live/signals.py | 119 +- .../install_live_branch_schedule.ps1 | 10 +- operator_tools/live_branch.cmd | 6 +- operator_tools/operator_live_branch.py | 131 +- tests/_dummy_v2_manifest.txt | 1 - tests/dummy_blunder.py | 1 - ...apital-envelope-cap-order-negative.v1.json | 23 - .../dumbmoney/signed-capital-envelope.v1.json | 50 - tests/test_dumbmoney_command_feed.py | 1119 ----------- tests/test_dumbmoney_contract_feed.py | 592 ------ tests/test_dumbmoney_direct_mcp.py | 894 -------- tests/test_dumbmoney_journal_anchor.py | 236 --- tests/test_dumbmoney_live_authority.py | 1126 ----------- tests/test_dumbmoney_windows_service.py | 770 ------- tests/test_no_live_artifact_bindings.py | 2 +- uv.lock | 1309 ------------ verifier/protected_manifest.py | 7 - 36 files changed, 231 insertions(+), 16805 deletions(-) delete mode 100644 configs/dumbmoney/dopey-robinhood-runner.v1.template.json delete mode 100644 configs/dumbmoney/schemas/dopey-robinhood-runner.v1.schema.json delete mode 100644 docs/DUMBMONEY_ROBINHOOD_CELL.md delete mode 100644 dopey_live/dumbmoney_authority.py delete mode 100644 dopey_live/dumbmoney_command_feed.py delete mode 100644 dopey_live/dumbmoney_contract_feed.py delete mode 100644 dopey_live/dumbmoney_journal_anchor.py delete mode 100644 dopey_live/dumbmoney_promotion.py delete mode 100644 dopey_live/dumbmoney_windows_service.py delete mode 100644 dopey_live/robinhood_mcp_direct.py delete mode 100644 tests/_dummy_v2_manifest.txt delete mode 100644 tests/dummy_blunder.py delete mode 100644 tests/fixtures/dumbmoney/capital-envelope-cap-order-negative.v1.json delete mode 100644 tests/fixtures/dumbmoney/signed-capital-envelope.v1.json delete mode 100644 tests/test_dumbmoney_command_feed.py delete mode 100644 tests/test_dumbmoney_contract_feed.py delete mode 100644 tests/test_dumbmoney_direct_mcp.py delete mode 100644 tests/test_dumbmoney_journal_anchor.py delete mode 100644 tests/test_dumbmoney_live_authority.py delete mode 100644 tests/test_dumbmoney_windows_service.py delete mode 100644 uv.lock diff --git a/.github/ci/fast_subset.txt b/.github/ci/fast_subset.txt index 8599a88..19c6674 100644 --- a/.github/ci/fast_subset.txt +++ b/.github/ci/fast_subset.txt @@ -89,11 +89,3 @@ tests/test_ci_and_epistemic_ledger_stability.py tests/test_epistemic_adversarial_supervision.py tests/test_epistemic_runtime_integration.py tests/test_squeeze_bubble_epistemic_hardening.py -tests/test_dumbmoney_live_authority.py -tests/test_dumbmoney_command_feed.py -tests/test_dumbmoney_contract_feed.py -tests/test_dumbmoney_direct_mcp.py -tests/test_dumbmoney_journal_anchor.py -tests/test_dumbmoney_windows_service.py -tests/test_live_cash_settlement_and_auth.py -tests/test_live_owner_liquidation_and_legacy_twin.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 84b7618..770cfff 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -159,7 +159,6 @@ jobs: ruff check . --select E9,F63,F7,F82 ruff check \ dopey \ - dopey_live \ dopey_acceleration \ dopey_execution \ dopey_governor \ diff --git a/PROTECTED_MANIFEST.json b/PROTECTED_MANIFEST.json index e4d40cf..1826b9e 100644 --- a/PROTECTED_MANIFEST.json +++ b/PROTECTED_MANIFEST.json @@ -67,21 +67,14 @@ "dopey_governor/live_order_gate.py": "b3a06c62d3218a54ba28f84cdb5eaee4e345986690e9482eef19e760adfcaa28", "dopey_governor/promotion_policy.py": "a2d650285c2cde579378226758732e82be94f8396b8491f7bf0a09199eef8489", "dopey_governor/trade_permission_kernel.py": "8be9e4167151da04b0b01d229aa76d23b9eae3454c0452184368ff7f8047ac6f", - "dopey_live/__init__.py": "3cc132b1af4c6e67627d8e3390830ea64c3c11f58b38dd17cd69c89f14d967ba", + "dopey_live/__init__.py": "72a5cd8e2f1b1b38661c4af97a9101fa522a896b26de61982d1ee3676efe0d40", "dopey_live/control.py": "047b57171821bd0dedb26024b21a8a9c1365e398881a2182c6d87830184b0286", - "dopey_live/dumbmoney_authority.py": "214c3cd32e6e3ddca6f188d8301251433494ff0cad61cacedf4d8adb941ca554", - "dopey_live/dumbmoney_command_feed.py": "63b5ae71fdea758004f303521ee4c3c1b499e98ae4d8010ad3e9ea80ea122e74", - "dopey_live/dumbmoney_contract_feed.py": "e7a00f12afbfff4de1dead1e4685c6421f4d1e932371bb7a6de4a034a1c8b927", - "dopey_live/dumbmoney_journal_anchor.py": "af45c1f00075deb1e6c03b17318228375a72dfd2c2550d8376bd5f023b4aebf5", - "dopey_live/dumbmoney_promotion.py": "b919df0ce3870f8c6490a8e24c8c52a64aa6f598979c469ecdc59760b0fef57f", - "dopey_live/dumbmoney_windows_service.py": "4cba589b6e0a05f69916fdd8ca45321898d8c8fc07a44c6f05a5385010bc0a47", - "dopey_live/ledger.py": "934d5a03e5a378400ec5fd787e270a948c57e0235615ca0b57ab78e6c1b993b7", - "dopey_live/models.py": "e1f78db857fae5c72ca368724b78096c5d3ba7b3497c5a52c29d9ddeca0f05a9", - "dopey_live/robinhood_gateway.py": "a12e7c8e60067fabca3fabd7d274cfc117bcf55ab6b71082f98c1855b73c1cff", - "dopey_live/robinhood_mcp_direct.py": "a181bebbb99e094440a2ae0353ad100a1883ce0c57bdef16c0c94ef6b69d6c7a", - "dopey_live/service.py": "899611c7d0dcd1240ef3467ec39f24defdc1b6cb754c64ad049407116e3f8b46", + "dopey_live/ledger.py": "228eaff3d398e0578a44a300223141835726033fc975955f90b689a68429681b", + "dopey_live/models.py": "938a660732c48e7198fb8b89ece16ba4392b5264083ad2088ce52ff576bc1bdc", + "dopey_live/robinhood_gateway.py": "6239e7b351623d5eb41a74caf73fcaa8dd3edd11fb3496982306b5b6970874b1", + "dopey_live/service.py": "fad0bccd3539d6d7968491245efd46857f31dbbbdddbe5019db2dcbc80c3f956", "dopey_live/settlement.py": "336892885775e3476010e5a6b319c9f4299c92758c0c7fc4f997787f049d8dd9", - "dopey_live/signals.py": "b45298ef50ae9d77766cb33575e71095870e4fddbfe12f9be662bbd63b4452d4", + "dopey_live/signals.py": "9bd1beb39b7769918f3e82c7b6492a804a95aea79c4457a85ce04a4ab1b6d778", "environments/environment_v1.json": "811d348d94e151087cd77d496bd1f5135b24dd40ba107b5d860cde34b8475652", "pyproject.toml": "07939e32fda357c13d347aecc5f95139d9d4aa236e29b7722ebb2fd378da1f27", "sealed_evaluator_client/__init__.py": "f97f1169fdf35b1331d93cb1147510d35e0ed7df89121ae162705bb645eff0aa", @@ -94,8 +87,8 @@ "verifier/environment_freeze.py": "e6310223af95c1fb4ba319400aaa335de2e23fe612d2186dcd9531778b39a177", "verifier/external_root.py": "e5eafa732082ca1a3e50456f42b0c249fc84979a15c04056439dd35594821945", "verifier/path_policy.py": "740d9947b7b12cec4db86f3aae9c9f6c26c029a7243efb047d1fe57ee39c3ea5", - "verifier/protected_manifest.py": "2dd350cbe84c3638c78d164a85acb7207a472d5398c593792aa913d18e2a6343" + "verifier/protected_manifest.py": "f0f5d1aa53739236cdedde3d3a3d57428bf1f6c5caaf6fab01f8f9e5979f4982" }, - "manifest_hash": "75408a8d57f7b349700f602574eabeab8142419a4af7085834ecfadfcb76123e", + "manifest_hash": "11da8aec9fedb8b8cea4dc9a698fdf68fb2798d23dac69dae28177392240f2d1", "schema_version": "dopey-protected-manifest-v1" } diff --git a/PUBLIC_SOURCE_MANIFEST.json b/PUBLIC_SOURCE_MANIFEST.json index 8212e87..a5ce517 100644 --- a/PUBLIC_SOURCE_MANIFEST.json +++ b/PUBLIC_SOURCE_MANIFEST.json @@ -17,7 +17,7 @@ "operator_agent_configuration_included": false, "aggregate_non_account_validation_metadata_included": true, "source_inventory": { - "public_file_count": 2700, + "public_file_count": 2679, "public_source_size_class": "under_50_mib", "excluded_file_count": 4514 }, diff --git a/configs/dumbmoney/dopey-robinhood-runner.v1.template.json b/configs/dumbmoney/dopey-robinhood-runner.v1.template.json deleted file mode 100644 index 4165a95..0000000 --- a/configs/dumbmoney/dopey-robinhood-runner.v1.template.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "schema": "dopey.dumbmoney-robinhood-runner-config.v1", - "service_name": "DumbMoneyDopeyRobinhood", - "release_id": "TO_BE_RESOLVED", - "core_endpoint_ref": "endpoint-ref:DumbMoneyCore", - "core_readiness_path": "C:\\ProgramData\\DumbMoney\\readiness\\DumbMoneyCore.json", - "core_cell_token_target": "credential-target:DumbMoney/DopeyCellToken", - "robinhood_profile_target": "credential-target:DumbMoney/RobinhoodProfile", - "start_mode": "RECONCILIATION_ONLY", - "data_root": "C:\\ProgramData\\DumbMoney\\dopey-robinhood", - "readiness_ref": "readiness-ref:DumbMoneyDopeyRobinhood", - "readiness_path": "C:\\ProgramData\\DumbMoney\\readiness\\DumbMoneyDopeyRobinhood.json", - "core_public_key_base64url": "TO_BE_RESOLVED", - "operator_public_keys_base64url": { - "TO_BE_RESOLVED": "TO_BE_RESOLVED" - }, - "promoter_public_keys_base64url": { - "TO_BE_RESOLVED": "TO_BE_RESOLVED" - }, - "research_public_keys_base64url": { - "TO_BE_RESOLVED": "TO_BE_RESOLVED" - }, - "evaluator_public_keys_base64url": { - "TO_BE_RESOLVED": "TO_BE_RESOLVED" - }, - "expected_account_hash": "TO_BE_RESOLVED", - "fund_lock_sha256": "TO_BE_RESOLVED", - "service_manifest_sha256": "TO_BE_RESOLVED", - "role_public_keys_sha256": "TO_BE_RESOLVED", - "core_runner_config_sha256": "TO_BE_RESOLVED", - "risk_policy_sha256": "TO_BE_RESOLVED", - "readiness_signer_public_key_sha256": "TO_BE_RESOLVED", - "codex_executable_path": "TO_BE_RESOLVED", - "codex_executable_sha256": "TO_BE_RESOLVED", - "codex_local_provider": "ollama", - "codex_local_model": "TO_BE_RESOLVED", - "codex_local_model_digest": "TO_BE_RESOLVED", - "ollama_executable_path": "TO_BE_RESOLVED", - "ollama_executable_sha256": "TO_BE_RESOLVED", - "ollama_process_identity_sid": "TO_BE_RESOLVED", - "ollama_runtime_evidence_sha256": "TO_BE_RESOLVED", - "poll_interval_seconds": 15, - "readiness_ttl_seconds": 90 -} diff --git a/configs/dumbmoney/schemas/dopey-robinhood-runner.v1.schema.json b/configs/dumbmoney/schemas/dopey-robinhood-runner.v1.schema.json deleted file mode 100644 index 6e403e0..0000000 --- a/configs/dumbmoney/schemas/dopey-robinhood-runner.v1.schema.json +++ /dev/null @@ -1,183 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://obtuse.ai/schemas/dumbmoney/dopey-robinhood-runner.v1.schema.json", - "title": "DumbMoney Dopey Robinhood Windows runner public config", - "type": "object", - "additionalProperties": false, - "required": [ - "schema", - "service_name", - "release_id", - "core_endpoint_ref", - "core_readiness_path", - "core_cell_token_target", - "robinhood_profile_target", - "start_mode", - "data_root", - "readiness_ref", - "readiness_path", - "core_public_key_base64url", - "operator_public_keys_base64url", - "promoter_public_keys_base64url", - "research_public_keys_base64url", - "evaluator_public_keys_base64url", - "expected_account_hash", - "fund_lock_sha256", - "service_manifest_sha256", - "role_public_keys_sha256", - "core_runner_config_sha256", - "risk_policy_sha256", - "readiness_signer_public_key_sha256", - "codex_executable_path", - "codex_executable_sha256", - "codex_local_provider", - "codex_local_model", - "codex_local_model_digest", - "ollama_executable_path", - "ollama_executable_sha256", - "ollama_process_identity_sid", - "ollama_runtime_evidence_sha256", - "poll_interval_seconds", - "readiness_ttl_seconds" - ], - "properties": { - "schema": { - "const": "dopey.dumbmoney-robinhood-runner-config.v1" - }, - "service_name": { - "const": "DumbMoneyDopeyRobinhood" - }, - "release_id": { - "type": "string", - "minLength": 1, - "maxLength": 128 - }, - "core_endpoint_ref": { - "const": "endpoint-ref:DumbMoneyCore" - }, - "core_readiness_path": { - "const": "C:\\ProgramData\\DumbMoney\\readiness\\DumbMoneyCore.json" - }, - "core_cell_token_target": { - "const": "credential-target:DumbMoney/DopeyCellToken" - }, - "robinhood_profile_target": { - "const": "credential-target:DumbMoney/RobinhoodProfile" - }, - "start_mode": { - "const": "RECONCILIATION_ONLY" - }, - "data_root": { - "const": "C:\\ProgramData\\DumbMoney\\dopey-robinhood" - }, - "readiness_ref": { - "const": "readiness-ref:DumbMoneyDopeyRobinhood" - }, - "readiness_path": { - "const": "C:\\ProgramData\\DumbMoney\\readiness\\DumbMoneyDopeyRobinhood.json" - }, - "core_public_key_base64url": { - "$ref": "#/$defs/public_key" - }, - "operator_public_keys_base64url": { - "$ref": "#/$defs/key_map" - }, - "promoter_public_keys_base64url": { - "$ref": "#/$defs/key_map" - }, - "research_public_keys_base64url": { - "$ref": "#/$defs/key_map" - }, - "evaluator_public_keys_base64url": { - "$ref": "#/$defs/key_map" - }, - "expected_account_hash": { - "$ref": "#/$defs/sha256" - }, - "fund_lock_sha256": { - "$ref": "#/$defs/sha256" - }, - "service_manifest_sha256": { - "$ref": "#/$defs/sha256" - }, - "role_public_keys_sha256": { - "$ref": "#/$defs/sha256" - }, - "core_runner_config_sha256": { - "$ref": "#/$defs/sha256" - }, - "risk_policy_sha256": { - "$ref": "#/$defs/sha256" - }, - "readiness_signer_public_key_sha256": { - "$ref": "#/$defs/sha256" - }, - "codex_executable_path": { - "type": "string", - "minLength": 3, - "maxLength": 1024 - }, - "codex_executable_sha256": { - "$ref": "#/$defs/sha256" - }, - "codex_local_provider": { - "const": "ollama" - }, - "codex_local_model": { - "type": "string", - "pattern": "^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)*:[a-z0-9][a-z0-9._-]{0,63}$", - "not": { - "pattern": ":(latest|cloud)$" - } - }, - "codex_local_model_digest": { - "$ref": "#/$defs/sha256" - }, - "ollama_executable_path": { - "type": "string", - "minLength": 3, - "maxLength": 1024 - }, - "ollama_executable_sha256": { - "$ref": "#/$defs/sha256" - }, - "ollama_process_identity_sid": { - "type": "string", - "pattern": "^S-1-5-([0-9]+-){1,14}[0-9]+$" - }, - "ollama_runtime_evidence_sha256": { - "$ref": "#/$defs/sha256" - }, - "poll_interval_seconds": { - "type": "integer", - "minimum": 5, - "maximum": 300 - }, - "readiness_ttl_seconds": { - "type": "integer", - "minimum": 10, - "maximum": 120 - } - }, - "$defs": { - "sha256": { - "type": "string", - "pattern": "^[0-9a-f]{64}$" - }, - "public_key": { - "type": "string", - "pattern": "^[A-Za-z0-9_-]{43}$" - }, - "key_map": { - "type": "object", - "minProperties": 1, - "maxProperties": 16, - "additionalProperties": false, - "patternProperties": { - "^[0-9a-f]{64}$": { - "$ref": "#/$defs/public_key" - } - } - } - } -} diff --git a/docs/DUMBMONEY_ROBINHOOD_CELL.md b/docs/DUMBMONEY_ROBINHOOD_CELL.md deleted file mode 100644 index 264d777..0000000 --- a/docs/DUMBMONEY_ROBINHOOD_CELL.md +++ /dev/null @@ -1,437 +0,0 @@ -# DumbMoney Robinhood venue cell - -`dopey_live` is the sovereign Robinhood execution cell for DumbMoney. The -older `dopey_execution` Robinhood adapter remains a discovery-only denial stub -and is not imported or routed by this integration. DumbMoney Core can narrow -DOPEY authority; it cannot enlarge the owner control, broker account scope, -settled cash, local risk policy, research gates, or broker capabilities. - -## Release status - -The private-local Windows service entrypoint is -`dopey_live.dumbmoney_windows_service:main`, packaged as -`DumbMoneyDopeyRobinhood.exe`. It is separate from the inherited operator CLI; -that CLI cannot satisfy the service credential, discovery, or freshness -contract and remains fail-closed in strict mode. - -Importing any DumbMoney module starts no listener, timer, thread, network -request, or broker call. Service startup validates configuration and -credentials, acquires its ProgramData lease, starts a loopback-only health -endpoint, and writes a signed `RECONCILIATION_ONLY` readiness descriptor. It -waits one poll interval before its first read-only reconciliation. New exposure -can be considered only on a later cycle. - -The legacy standalone operator remains available outside strict release mode. -A manually supplied signed capital envelope can be used for preflight: - -```powershell -py -3.12 -m operator_tools.operator_live_branch ` - --dumbmoney-envelope C:\secure\capital-envelope.json ` - --dumbmoney-public-key C:\secure\dumbmoney-core.pub ` - --dumbmoney-account-hash -``` - -The legacy CLI is read-only preflight: every `--execute` invocation is rejected -before gateway or model construction, including programmatic calls through its -crash-tombstone wrapper. Production execution exists only in the supervised -Windows service. A static envelope is not the production Core command channel. -The manually supplied envelope file must be exact canonical UTF-8 JSON: -duplicate keys, non-finite numbers, floats, alternate key order/whitespace, a -byte-order mark, and trailing bytes are rejected. - -## Authenticated pull-only command feed - -Core exposes the local-only endpoint: - -```text -GET http://127.0.0.1:/v1/cells/dopey_robinhood/commands - ?after=&cursor=&limit= - &request_nonce=<64-lowercase-hex> -``` - -Use `DumbMoneyCommandFeedClient` to bind deployment-owned dependencies. The -bearer provider must read the per-cell secret from an OS-backed secret store; -the bearer is never accepted from a CLI flag, environment variable, persisted -state, log, or artifact. The HTTP transport is also injected, so library import -cannot contact Core. - -```python -from datetime import datetime -from pathlib import Path - -from dopey_live import ( - DumbMoneyCommandFeedClient, - DumbMoneyContractResolverClient, -) -from dopey_live.dumbmoney_command_feed import command_feed_state_path -from operator_tools.operator_live_branch import run_supervised_dumbmoney_cycle - -repository_root = Path(r"C:\path\to\dopey") -client = DumbMoneyCommandFeedClient( - base_url="http://127.0.0.1:8765", - cell_bearer_provider=os_secret_provider, - trusted_public_key=trusted_core_public_key, - trusted_operator_public_keys=sealed_operator_public_key_map, - expected_account_hash=pinned_agentic_account_hash, - state_path=command_feed_state_path(repository_root), - transport=loopback_http_transport, -) -resolver = DumbMoneyContractResolverClient( - base_url="http://127.0.0.1:8765", - cell_bearer_provider=os_secret_provider, - trusted_core_public_key=trusted_core_public_key, - trusted_promoter_public_keys=sealed_promoter_public_key_map, - trusted_research_public_keys=sealed_research_public_key_map, - trusted_evaluator_public_keys=sealed_evaluator_public_key_map, - transport=loopback_http_transport, -) - -# The helper polls under the Live Branch process lock, then passes the same -# callable to the broker-sink guard for immediate pre-spawn and post-return polls. -result = run_supervised_dumbmoney_cycle( - client.poll_once, - resolver, - execute_orders=False, -) -``` - -`os_secret_provider`, the Core/operator/promoter/research/evaluator public -keys, the account hash, and `loopback_http_transport` are deployment -dependencies, not repository defaults. The production Windows runner supplies -these bindings from one exact-SHA public config and two Credential Manager -targets. - -The consumer: - -- restricts Core to an exact `http://127.0.0.1:` origin; -- sends the injected bearer only in the request authorization header; -- generates a fresh cryptographically random 32-byte request nonce for every - poll and rejects any top-level or signed-checkpoint echo mismatch; -- verifies the page checkpoint with the pinned Core key; -- recomputes every full ledger-event proof and binds its event identity, - source sequence, signer, body digest, timestamps, causality, and hash-chain - material to the original signed envelope; -- accepts capital envelopes only from the Core key and kill/desired-mode - envelopes only from the disjoint sealed operator key set; -- enforces per-control monotonic generation/revision continuity; -- advances a digest cursor and atomically replaces its local state file; -- re-verifies persisted commands after restart; -- replays expired signed history to recover sequence state, while never - granting stale/future LIVE mode or capital; -- preserves an expired active kill as active until a newer signed generation - clears it; -- maps authentication, transport, state, schema, signature, gap, cursor, - pagination, omission, and unknown failures to `kill_active=True` or - paused/unknown, never to positive authority. - -The Core-signed checkpoint covers the request cursor, ordered command -projections and complete ledger proofs, next cursor, ledger head, observation -time, required action, and per-request nonce. Page fields must exactly match -that checkpoint. This prevents an untrusted transport from replaying, omitting, -or rewriting a command. Core and its signing key remain the root control-plane -trust boundary, so keep them inside the protected host and prove kill latency -during the attended canary. - -## Core-backed local rollback anchors - -Before any broker reconciliation and again after command-state and cycle -mutations, the Windows runner publishes integrity-checked heads for the command -cursor, live-branch ledger, and paper-twin ledger to Core's authenticated -`/v1/cells/dopey_robinhood/journal-heads:anchor` route. Each request binds the -pinned account, deterministic journal stream, local sequence, local head, and -fresh nonce. Dopey accepts the result only after verifying the Core-signed -anchor envelope, request-bound checkpoint, and recomputed global-ledger proof. - -Core rejects a lower sequence or a different head at the same sequence. Dopey -therefore detects restored, truncated, or forked local state even if the local -files remain internally hash-consistent. A rejected pre-cycle anchor returns -`blocked_rollback_anchor` before any Robinhood contact. A failed post-cycle -anchor makes readiness `BLOCKED`, disables the following execution cycle, and -preserves the cycle result for reconciliation. `/health/ready` requires current -anchors, complete reconciliation, and `execution_enabled=true`; mere process -liveness or read-only reconciliation is not execution readiness. - -## Capital and promotion authority - -`CapitalEnvelopeV1` is Ed25519-signed, account-bound, and valid for at most 60 -seconds. The cell verifies it at cycle allocation, at broker-sink preparation, -and again through the refreshed command feed immediately before the external -write boundary. - -The contract requires: - -- exactly one `strategy_hash`, one `passport_hash`, and one `promotion_hash`, - forming a single non-mixable tuple per capital lease; -- sorted, nonempty, exact Robinhood sink identities: - `equity:` or - `option::`; -- `max_order_risk_cents <= max_correlated_risk_cents <= - max_open_risk_cents`; -- positive open-order, daily-loss, generation, and policy-epoch limits. - -Wildcards, symbol prefixes, instrument-class-only grants, source-hash aliases, -and multi-candidate capital sets are rejected. Local signal builders leave -`passport_hash` and `promotion_hash` empty. The resolver derives both values; -caller-supplied values are rejected. - -For each buy, Dopey calls the authenticated Core route: - -```text -GET /v1/cells/dopey_robinhood/contracts/ - ?request_nonce=<64-lowercase-hex> - &capital_envelope_digest= -``` - -It verifies the nonce-bound Core checkpoint, complete recomputed ledger proof, -active capital digest and fencing generation, original signed envelope, and -currentness projection. Promotion certificates must come from the disjoint -sealed promoter key set; passports must come from the disjoint sealed research -key set. Each resolution must also contain the Core-signed -`dumbmoney.cell-authority-state.v1` projection for the same ledger state. Dopey -requires current LIVE mode, clear kill state, the exact mandate, policy epoch, -capital fence, strategy/passport/promotion tuple, at least two unique current -PASS verdict facts from distinct courts and sealed evaluator signer IDs, and a -ledger head continuous with the requested contract proof. The promotion and -passport reads must resolve against one identical authority state. Venue, -exact instrument, policy epoch, strategy, passport link, and maximum loss must -agree with the active singleton capital tuple. The full resolution occurs at -preflight, broker-sink preparation, and inside each final broker-boundary -guard. A changed event, signer, lineage, verdict, ledger head, capital fence, -or expiry fails closed. - -The canonical cross-repository positive fixture is -`tests/fixtures/dumbmoney/signed-capital-envelope.v1.json`, whose file SHA-256 -is -`b0fe2fa0261b2dc36e4043e8c33b91b1016676e04a25e4b4d5bd66eea0c9079a`. -Local negative conformance cases cover both invalid cap-order directions. - -A persisted fence binds the complete authority identity: - -```text -(envelope_id, event_id, body_digest, fencing_generation, policy_epoch) -``` - -Reusing one generation with any different identity is rejected, and a stale -generation cannot regain authority after process-lock reacquisition. - -## Deterministic broker-write checkpoint and revocation window - -The generative subprocess receives neither the Robinhood bearer nor the -Robinhood MCP endpoint. Broker reads and writes occur only in the parent -process through `DeterministicRobinhoodMcpClient`; a configured parent -transport cannot fall back to a model-selected tool call. - -[Robinhood's official Agentic overview](https://robinhood.com/us/en/support/articles/agentic-trading-overview/) -says an authenticated agent may read all Robinhood accounts but can place -trades only in its Agentic account. Dopey therefore calls `get_accounts` inside -every write session, requires exactly one explicitly labeled Agentic account, -and binds its SHA-256 to the account hash sealed in both runner config and -capital authority. The current equity write schemas expose no account -selector; adding one is schema drift and blocks the call. This same-session -proof plus Robinhood's server-side Agentic-only write contract prevents silent -default-account selection. - -The phase-one write path is deliberately equity-only: - -1. persist `DISPATCHING` with zero placement retries; -2. refresh kill, mode, capital, exact authority identity, fence, promotion, - passport, local kill, and current account evidence; -3. require more than the configured 30-second write timeout plus a 5-second - safety margin on the authority lease; -4. construct one exact quantity-mode equity argument object locally; -5. open a one-shot official Streamable HTTP MCP session, verify the live - `review_equity_order` and `get_accounts` schemas, prove the pinned Agentic - account, and call review exactly once; -6. bind the typed review response to the exact local arguments and require - explicit approval; -7. refresh the complete signed authority again after review and immediately - before placement; -8. open a second one-shot MCP session, verify the live place/account schemas, - prove the same pinned Agentic account, refresh authority after schema - verification, and call `place_equity_order` exactly once with the identical - argument object; -9. never retry an attempted placement; an incomplete, conflicting, timed-out, - or non-attested response is ambiguous and enters reconcile/cancel; -10. refresh controls again after return. - -The direct client disables proxy inheritance and redirects, pins the MCP -protocol version, checks request IDs and typed JSON/SSE responses, closes -initialized sessions in a `finally` path, rejects tool schema drift before the -write call, and exposes only sealed read tools plus the three equity -review/place/cancel names to parent code. Parent reads preserve broker evidence -without model normalization: negative/overflow balances, missing or -conflicting position economics, incomplete history, bad timestamps, and -missing/extra quote symbols fail closed. Option writes remain blocked until -their exact current schemas, contract identity, quotes, and result semantics -receive the same deterministic adapter. - -A kill that arrives after step 7 can still race the single outbound placement -request. The system does not claim instantaneous revocation. The bounded -timeout, at-most-once dispatch, ambiguous state, post-return poll, and -immediate cancel/reconcile remain the truthful in-flight guarantees. - -## Risk and sell-side behavior - -New exposure intersects the signed grant with settled cash, complete broker -positions, all active orders, worst-case intent loss, and witnessed daily loss. -Worst-case buy risk must fit the remaining daily-loss capacity; equality at -the boundary is allowed. Missing broker fee evidence or missing unrealized PnL -blocks the calculation rather than being represented as zero. - -Until a signed cross-account risk snapshot carries independently witnessed -cluster attribution, DOPEY conservatively applies the correlated-risk ceiling -to total observed open risk. It does not claim a reconciliation receipt digest, -high-water drawdown, or true cluster-level attribution it cannot witness. - -Sells are risk-reducing only. A sell requires exactly one fresh matching long -broker position, a positive finite quantity no greater than the witnessed -holding, and no active sell for the same stock or exact option contract. Naked -sells, oversells, and duplicate exits fail closed. - -## Kill and degraded operation - -Either a signed feed kill or the local file -`artifacts/dopey/live_branch/DUMBMONEY_KILL.v1` blocks new exposure. An active -kill: - -1. attempts one deterministic official-MCP cancellation for each active, - DOPEY-attributed equity order; -2. polls complete order history at most three times; -3. reports `CANCEL_ONLY_DEGRADED` until history is terminal and reconciled. - -Production cancellation uses only the sealed `cancel_equity_order` parent-side -adapter. Option cancellation remains blocked until it receives the same -deterministic schema and result attestation. The cancel path never reviews, -replaces, or places an order. Unattributed orders, unidentified ambiguous -submissions, option orders, and residual positions remain attended blockers; -they are not silently represented as canceled or flat. - -Ambiguous placement is never retried. The cell resolves it automatically only -when bounded, complete read-only broker history supplies one exact match for -account, instrument, contract, side, quantity/notional, limit, time-in-force, -market date, and the submission window. Every other case remains -operator-visible and cancel-only. - -## Laptop migration and attended canary - -Move non-secret DOPEY data from the laptop as a content-hashed, staged import -under `C:\ProgramData\DumbMoney\dopey-robinhood`; do not copy OAuth cookies, -browser profiles, bearer tokens, or plaintext credentials. - -## Private-local Windows runner - -The runner loads only: - -- the exact raw bytes of - `C:\ProgramData\DumbMoney\config\dopey-robinhood-runner.v1.json`, whose - lowercase SHA-256 must equal `--config-sha256`; -- the cell bearer from Windows Credential Manager target - `DumbMoney/DopeyCellToken`; -- the official Robinhood MCP bearer profile from Windows Credential Manager - target `DumbMoney/RobinhoodProfile`. - -There is no environment-variable, plaintext-file, hosted coordinator, -telemetry, or public-repository secret fallback. -Core/operator/promoter/research/evaluator public keys, the account hash, Core -readiness pins, release identity, and fixed ProgramData paths are public -config. `role_public_keys_sha256` is the canonical SHA-256 of Core's complete -`role_public_keys_base64url` object. The release must derive the Dopey -readiness signer from the cell credential and require its public-key ID to -equal the sole `dopey_venue` role key; an ambiguous role set fails release -assembly. The absolute Codex child executable and its SHA-256 are also pinned -there; the runner re-hashes it at construction and immediately before every -child spawn, so the credential-bearing process is never selected through -`PATH`. The sealed release installs that dependency at -`C:\Program Files\DumbMoney\releases\\tools\codex\codex.exe`; -the runner does not download or fabricate it. - -Any local generative Codex execution is forced to `--oss --local-provider -ollama` at the literal `http://127.0.0.1:11434/v1` endpoint, without Robinhood -MCP configuration or broker credentials. It never inherits `HOME`, -`USERPROFILE`, `CODEX_HOME`, `OPENAI_API_KEY`, or a hosted-provider credential. -The public config pins one explicit, non-`latest`, non-`cloud` Ollama model tag -and its 64-character manifest digest, plus the absolute Ollama server -executable and its SHA-256. Immediately before every Codex spawn, -the runner uses the Windows TCP owner table to require exactly one -`127.0.0.1:11434` listener, resolves its process image, requires the pinned -absolute path and binary hash, reads the loopback-only `/api/tags` inventory, -requires exactly one matching tag/model/digest tuple, then re-checks that the -same PID, process SID, image, and hash still own the listener. Missing, -ambiguous, changed, wildcard-bound, wrong-owner, or path-hijacked local model -state blocks the broker operation before the subprocess. - -The config also pins the raw SHA-256 of the static installation attestation at -`C:\ProgramData\DumbMoney\dopey-robinhood\evidence\ollama-runtime-evidence.v1.json`. -The release artifact binds the dedicated non-service local process identity, -loopback-only/no-egress policy, executable, ACL-protected model-store owner and -SDDL digest, model-store manifest, and model tag/digest. Dopey hashes that -artifact at use time and then independently revalidates the live listener PID, -owner image, executable hash, and model inventory; the static artifact contains -no restart-volatile PID. -OpenRouter is therefore outside this privileged execution transport; it may be -budgeted for research and planning without becoming Robinhood order authority. - -The config schema and release template are -`configs/dumbmoney/schemas/dopey-robinhood-runner.v1.schema.json` and -`configs/dumbmoney/dopey-robinhood-runner.v1.template.json`. - -Core discovery reads only the current Core-signed descriptor at -`C:\ProgramData\DumbMoney\readiness\DumbMoneyCore.json`. Dopey verifies its -Core signature, release and artifact pins, short lifetime, broker-none -authority, and literal loopback endpoint before calling `/health/live`, -`/v1/cells/dopey_robinhood/commands`, or the contract resolver. It never -receives the operator token and does not treat the unsigned Core health route -as authority; the nonce-bound Core command checkpoint is required. - -The service writes a short-lived Ed25519-signed descriptor to -`C:\ProgramData\DumbMoney\readiness\DumbMoneyDopeyRobinhood.json`. Its signing -key is domain-separated from the Credential Manager cell bearer, and the -derived public-key SHA-256 is pinned in the public config. Readiness reports -`execution_enabled=false` and mode `RECONCILIATION_ONLY` until a complete -broker reconciliation, all current signed controls, and all three Core-backed -local journal anchors agree. - -One Windows byte-range lease on the exact ProgramData root excludes concurrent -service writers; the OS releases it on process death, and a restart must -reacquire it before any cycle. Intent reservation and fence activation are -read-check-append transactions under the cross-process ledger lock and are -flushed to disk. An interrupted reservation stays unresolved and blocks a new -dispatch until broker reconciliation. The final guard re-reads the current -fence under that lock, polls fresh Core authority, checks the local kill file, -and runs both before review and again immediately before the one placement -call. Cross-process regression tests prove a second writer is denied while the -lease is held and can acquire only after the first process exits. - -The runner gives the Robinhood profile only to the deterministic parent-side -MCP client. No generative process receives the bearer, the MCP endpoint, an -account identifier, or a broker tool. The bearer is never placed in argv, -prompt, ledger, readiness, logs, artifacts, or subprocess environment. -Parent-side MCP evidence remains in memory and only typed redacted snapshots -enter Dopey's ledger. - -Before the first live canary: - -- install the exact-SHA public config with disjoint Core/operator/promoter/ - research/evaluator public-key roles, the exact canonical Core role-bundle - digest, the sole matching Dopey venue readiness key, and the dedicated - Agentic account hash; -- provision the per-cell bearer in the OS secret provider and test 401/403, - timeout, restart, stale-state, journal rollback/fork, cursor-gap, kill, and - kill-clear behavior; -- provision the Robinhood profile only in Windows Credential Manager and - verify it is not inherited from environment or plaintext files; -- install the sealed `DumbMoneyDopeyRobinhood.exe` service runner with the - exact config SHA argument; -- install the release-selected tool-capable Ollama model locally, populate its - explicit tag, `/api/tags` digest, absolute server executable, and executable - SHA-256 in the public config, and deny Ollama cloud models and non-loopback - listeners; -- prove broker history exposes explicit per-fill fees, or add an attested - realized-PnL source; missing fee evidence blocks new exposure after a fill; -- add transcript-attested `get_option_quotes` evidence before enabling options; -- perform attended place, cancel, history-convergence, restart, pre-spawn kill, - and in-flight kill drills at minimum capital; -- keep external confirmation that no unattributed or ambiguous order remains. - -These are activation requirements, not profitability or broker-readiness -claims. diff --git a/dopey_live/__init__.py b/dopey_live/__init__.py index 16c772f..d607e67 100644 --- a/dopey_live/__init__.py +++ b/dopey_live/__init__.py @@ -1,34 +1,6 @@ """DOPEY Live Branch: official Robinhood Agentic execution with a paper twin.""" from .control import LiveBranchControl, load_live_branch_control -from .dumbmoney_authority import ( - CAPITAL_ENVELOPE_SCHEMA, - DESIRED_MODE_SCHEMA, - KILL_STATE_SCHEMA, - SIGNED_ENVELOPE_SCHEMA, - DumbMoneyAuthorityContext, -) -from .dumbmoney_command_feed import ( - DumbMoneyCommandDispatch, - DumbMoneyCommandFeedClient, - poll_once, -) -from .dumbmoney_contract_feed import DumbMoneyContractResolverClient -from .dumbmoney_journal_anchor import CoreJournalAnchorClient from .service import run_live_branch_cycle -__all__ = [ - "CAPITAL_ENVELOPE_SCHEMA", - "DESIRED_MODE_SCHEMA", - "KILL_STATE_SCHEMA", - "SIGNED_ENVELOPE_SCHEMA", - "CoreJournalAnchorClient", - "DumbMoneyAuthorityContext", - "DumbMoneyCommandDispatch", - "DumbMoneyCommandFeedClient", - "DumbMoneyContractResolverClient", - "LiveBranchControl", - "load_live_branch_control", - "poll_once", - "run_live_branch_cycle", -] +__all__ = ["LiveBranchControl", "load_live_branch_control", "run_live_branch_cycle"] diff --git a/dopey_live/dumbmoney_authority.py b/dopey_live/dumbmoney_authority.py deleted file mode 100644 index 9e85fce..0000000 --- a/dopey_live/dumbmoney_authority.py +++ /dev/null @@ -1,1172 +0,0 @@ -"""DumbMoney capital authority for the sovereign Robinhood venue cell. - -The fund control plane can grant *less* authority than DOPEY's local controls, -never more. The signed envelope is verified at allocation time and again at -the broker sink so an expired or superseded lease cannot be used after a slow -research or quote step. -""" - -from __future__ import annotations - -import base64 -import hashlib -import json -import math -import re -from dataclasses import asdict, dataclass -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import Any, Final - -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey - -from .models import AccountSnapshot, LiveExecutionIntent - - -SIGNED_ENVELOPE_SCHEMA: Final[str] = "dumbmoney.signed-envelope.v1" -CAPITAL_ENVELOPE_SCHEMA: Final[str] = "dumbmoney.capital-envelope.v1" -DESIRED_MODE_SCHEMA: Final[str] = "dumbmoney.desired-mode.v1" -KILL_STATE_SCHEMA: Final[str] = "dumbmoney.kill-state.v1" -DOPEY_VENUE: Final[str] = "dopey_robinhood" -SIGNATURE_ALGORITHM: Final[str] = "Ed25519" -MAXIMUM_CAPITAL_ENVELOPE_TTL: Final[timedelta] = timedelta(seconds=60) -SHA256_HEX_LENGTH: Final[int] = 64 -IDENTIFIER_PATTERN: Final[re.Pattern[str]] = re.compile( - r"^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$" -) -UTC_TIMESTAMP_PATTERN: Final[re.Pattern[str]] = re.compile( - r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$" -) -EQUITY_INSTRUMENT_ID_PATTERN: Final[re.Pattern[str]] = re.compile( - r"^equity:[A-Z0-9][A-Z0-9.-]{0,31}$" -) -OPTION_INSTRUMENT_ID_PATTERN: Final[re.Pattern[str]] = re.compile( - r"^option:[A-Z0-9][A-Z0-9.-]{0,31}:" - r"[A-Za-z0-9][A-Za-z0-9._/-]{0,95}$" -) -SIGNED_ENVELOPE_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "source_id", - "source_sequence", - "event_id", - "correlation_id", - "causation_id", - "nonce", - "not_before", - "expires_at", - "body_schema", - "body_digest", - "body", - "signature_algorithm", - "signer_key_id", - "signature", - } -) -CAPITAL_ENVELOPE_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "envelope_id", - "mandate_id", - "venue", - "account_hash", - "strategy_hashes", - "passport_hashes", - "promotion_hashes", - "authorized_instruments", - "authorized_mode", - "max_order_risk_cents", - "max_open_risk_cents", - "max_correlated_risk_cents", - "max_daily_loss_cents", - "max_open_orders", - "fencing_generation", - "policy_epoch", - "not_before", - "expires_at", - } -) -DESIRED_MODE_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "venue", - "mode", - "revision", - "reason", - "policy_epoch", - "not_before", - "expires_at", - } -) -KILL_STATE_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "active", - "generation", - "reason", - "changed_by", - "policy_epoch", - "changed_at", - } -) -DESIRED_MODES: Final[frozenset[str]] = frozenset( - {"READ_ONLY", "PAPER", "LIVE", "PAUSED"} -) - - -class DumbMoneyAuthorityError(RuntimeError): - """Raised when global capital authority is absent, invalid, or exhausted.""" - - -@dataclass(frozen=True) -class CapitalEnvelopeV1: - schema: str - envelope_id: str - mandate_id: str - venue: str - account_hash: str - strategy_hashes: tuple[str, ...] - passport_hashes: tuple[str, ...] - promotion_hashes: tuple[str, ...] - authorized_instruments: tuple[str, ...] - authorized_mode: str - max_order_risk_cents: int - max_open_risk_cents: int - max_correlated_risk_cents: int - max_daily_loss_cents: int - max_open_orders: int - fencing_generation: int - policy_epoch: int - not_before: str - expires_at: str - - def to_dict(self) -> dict[str, Any]: - payload = asdict(self) - payload["strategy_hashes"] = list(self.strategy_hashes) - payload["passport_hashes"] = list(self.passport_hashes) - payload["promotion_hashes"] = list(self.promotion_hashes) - payload["authorized_instruments"] = list(self.authorized_instruments) - return payload - - -@dataclass(frozen=True) -class DesiredModeV1: - schema: str - venue: str - mode: str - revision: int - reason: str - policy_epoch: int - not_before: str - expires_at: str - - -@dataclass(frozen=True) -class KillStateV1: - schema: str - active: bool - generation: int - reason: str - changed_by: str - policy_epoch: int - changed_at: str - - -@dataclass(frozen=True) -class VerifiedCapitalAuthority: - envelope: CapitalEnvelopeV1 - event_id: str - source_id: str - source_sequence: int - signer_key_id: str - body_digest: str - - def evidence(self) -> dict[str, Any]: - return { - "envelope_id": self.envelope.envelope_id, - "mandate_id": self.envelope.mandate_id, - "event_id": self.event_id, - "source_id": self.source_id, - "source_sequence": self.source_sequence, - "signer_key_id": self.signer_key_id, - "body_digest": self.body_digest, - "fencing_generation": self.envelope.fencing_generation, - "policy_epoch": self.envelope.policy_epoch, - "expires_at": self.envelope.expires_at, - } - - -@dataclass(frozen=True) -class VerifiedDesiredMode: - desired_mode: DesiredModeV1 - event_id: str - source_id: str - source_sequence: int - signer_key_id: str - body_digest: str - not_before: str - expires_at: str - - -@dataclass(frozen=True) -class VerifiedKillState: - kill_state: KillStateV1 - event_id: str - source_id: str - source_sequence: int - signer_key_id: str - body_digest: str - not_before: str - expires_at: str - - -VerifiedControlEnvelope = ( - VerifiedCapitalAuthority | VerifiedDesiredMode | VerifiedKillState -) - - -@dataclass(frozen=True) -class DumbMoneyAuthorityContext: - """Explicit opt-in authority inputs; constructing this does not arm DOPEY.""" - - signed_envelope: dict[str, Any] - trusted_public_key: bytes - expected_account_hash: str - - @classmethod - def from_files( - cls, - envelope_path: Path, - public_key_path: Path, - expected_account_hash: str, - ) -> "DumbMoneyAuthorityContext": - try: - envelope_bytes = envelope_path.read_bytes() - except OSError as error: - raise DumbMoneyAuthorityError( - f"Unable to read DumbMoney envelope: path={envelope_path}, error={error}" - ) from error - payload = _loads_strict_signed_json( - envelope_bytes, - "DumbMoney signed envelope", - require_canonical_bytes=True, - ) - try: - encoded_key = public_key_path.read_bytes() - except OSError as error: - raise DumbMoneyAuthorityError( - f"Unable to read DumbMoney public key: path={public_key_path}, error={error}" - ) from error - return cls( - signed_envelope=dict(payload), - trusted_public_key=_decode_public_key(encoded_key), - expected_account_hash=_sha256_text( - expected_account_hash, - "expected_account_hash", - ), - ) - - def verify( - self, - now_utc: datetime, - observed_account_hash: str, - ) -> VerifiedCapitalAuthority: - if observed_account_hash != self.expected_account_hash: - raise DumbMoneyAuthorityError( - "Robinhood Agentic account hash does not match the pinned DumbMoney account" - ) - return verify_signed_capital_envelope( - self.signed_envelope, - self.trusted_public_key, - now_utc, - self.expected_account_hash, - ) - - -def canonical_json_bytes(payload: dict[str, Any]) -> bytes: - try: - return json.dumps( - payload, - sort_keys=True, - separators=(",", ":"), - ensure_ascii=False, - allow_nan=False, - ).encode("utf-8") - except (TypeError, ValueError) as error: - raise DumbMoneyAuthorityError( - f"DumbMoney payload is not canonical-JSON encodable: {error}" - ) from error - - -def _loads_strict_signed_json( - raw: bytes, - label: str, - *, - require_canonical_bytes: bool, -) -> dict[str, Any]: - def reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: - result: dict[str, Any] = {} - for key, value in pairs: - if key in result: - raise DumbMoneyAuthorityError( - f"{label} contains a duplicate JSON key: {key}" - ) - result[key] = value - return result - - def reject_constant(value: str) -> None: - raise DumbMoneyAuthorityError( - f"{label} contains a non-finite JSON number: {value}" - ) - - try: - text = raw.decode("utf-8") - payload = json.loads( - text, - object_pairs_hook=reject_duplicate_keys, - parse_constant=reject_constant, - ) - except (UnicodeDecodeError, json.JSONDecodeError) as error: - raise DumbMoneyAuthorityError(f"{label} must be valid UTF-8 JSON") from error - if not isinstance(payload, dict) or not all( - isinstance(key, str) for key in payload - ): - raise DumbMoneyAuthorityError(f"{label} must be a JSON object") - _validate_signed_json_domain(payload, label) - if require_canonical_bytes and raw != canonical_json_bytes(payload): - raise DumbMoneyAuthorityError(f"{label} must use exact canonical JSON bytes") - return payload - - -def _validate_signed_json_domain(value: Any, label: str, path: str = "$") -> None: - if value is None or isinstance(value, (str, bool, int)): - return - if isinstance(value, float): - if not math.isfinite(value): - raise DumbMoneyAuthorityError( - f"{label} {path} contains a non-finite JSON number" - ) - raise DumbMoneyAuthorityError( - f"{label} {path} contains a float outside the signed integer domain" - ) - if isinstance(value, list): - for index, item in enumerate(value): - _validate_signed_json_domain(item, label, f"{path}[{index}]") - return - if isinstance(value, dict): - if not all(isinstance(key, str) for key in value): - raise DumbMoneyAuthorityError( - f"{label} {path} contains a non-string object key" - ) - for key, item in value.items(): - _validate_signed_json_domain(item, label, f"{path}.{key}") - return - raise DumbMoneyAuthorityError( - f"{label} {path} contains unsupported JSON type {type(value).__name__}" - ) - - -def _decode_public_key(encoded: bytes) -> bytes: - if len(encoded) == 32: - return encoded - stripped = encoded.strip() - try: - text = stripped.decode("ascii") - except UnicodeDecodeError as error: - raise DumbMoneyAuthorityError( - "DumbMoney Ed25519 public key must be raw bytes, hex, or base64url" - ) from error - if len(text) == 64: - try: - decoded = bytes.fromhex(text) - except ValueError: - decoded = b"" - if len(decoded) == 32: - return decoded - try: - decoded = _urlsafe_b64decode(text) - except ValueError as error: - raise DumbMoneyAuthorityError( - "DumbMoney Ed25519 public key is not valid base64url" - ) from error - if len(decoded) != 32: - raise DumbMoneyAuthorityError( - f"DumbMoney Ed25519 public key must be 32 bytes: actual={len(decoded)}" - ) - return decoded - - -def _urlsafe_b64decode(value: str) -> bytes: - if ( - not value - or "=" in value - or not re.fullmatch(r"[A-Za-z0-9_-]+", value) - or len(value) % 4 == 1 - ): - raise ValueError("base64url must be unpadded") - try: - decoded = base64.urlsafe_b64decode(value + "=" * (-len(value) % 4)) - except (ValueError, TypeError) as error: - raise ValueError("invalid base64url") from error - canonical = base64.urlsafe_b64encode(decoded).decode("ascii").rstrip("=") - if canonical != value: - raise ValueError("base64url is not canonical") - return decoded - - -def _sha256_text(value: Any, label: str) -> str: - normalized = str(value).strip().lower() - if len(normalized) != SHA256_HEX_LENGTH: - raise DumbMoneyAuthorityError(f"{label} must be lowercase sha256 hex") - try: - bytes.fromhex(normalized) - except ValueError as error: - raise DumbMoneyAuthorityError( - f"{label} must be lowercase sha256 hex" - ) from error - if normalized != str(value): - raise DumbMoneyAuthorityError(f"{label} must be canonical lowercase sha256 hex") - return normalized - - -def account_hash(account_id: str) -> str: - if not account_id: - raise DumbMoneyAuthorityError("Robinhood Agentic account ID is missing") - return hashlib.sha256(account_id.encode("utf-8")).hexdigest() - - -def _utc_timestamp(value: Any, label: str) -> datetime: - text = str(value) - if not UTC_TIMESTAMP_PATTERN.fullmatch(text): - raise DumbMoneyAuthorityError( - f"{label} must be canonical RFC3339 UTC ending in Z" - ) - try: - parsed = datetime.fromisoformat(text[:-1] + "+00:00") - except ValueError as error: - raise DumbMoneyAuthorityError( - f"{label} is not a valid RFC3339 timestamp" - ) from error - if parsed.tzinfo is None or parsed.utcoffset() != timezone.utc.utcoffset(parsed): - raise DumbMoneyAuthorityError(f"{label} must use UTC") - canonical = parsed.isoformat( - timespec="microseconds" if parsed.microsecond else "seconds" - ).replace("+00:00", "Z") - if canonical != text: - raise DumbMoneyAuthorityError( - f"{label} must use canonical RFC3339 UTC precision" - ) - return parsed - - -def _exact_fields( - payload: dict[str, Any], expected: frozenset[str], label: str -) -> None: - missing = sorted(expected - set(payload)) - unknown = sorted(set(payload) - expected) - if missing or unknown: - raise DumbMoneyAuthorityError( - f"{label} schema mismatch: missing={missing}, unknown={unknown}" - ) - - -def _positive_int(payload: dict[str, Any], field: str) -> int: - value = payload.get(field) - if isinstance(value, bool) or not isinstance(value, int) or value <= 0: - raise DumbMoneyAuthorityError(f"{field} must be a positive integer") - return value - - -def _nonnegative_int(payload: dict[str, Any], field: str) -> int: - value = payload.get(field) - if isinstance(value, bool) or not isinstance(value, int) or value < 0: - raise DumbMoneyAuthorityError(f"{field} must be a nonnegative integer") - return value - - -def _hash_array(payload: dict[str, Any], field: str) -> tuple[str, ...]: - values = payload.get(field) - if not isinstance(values, list): - raise DumbMoneyAuthorityError(f"{field} must be an array") - normalized = tuple(_sha256_text(value, field) for value in values) - if not normalized: - raise DumbMoneyAuthorityError(f"{field} must be nonempty") - if normalized != tuple(sorted(set(normalized))): - raise DumbMoneyAuthorityError(f"{field} must be sorted and duplicate-free") - return normalized - - -def _identifier_array(payload: dict[str, Any], field: str) -> tuple[str, ...]: - values = payload.get(field) - if not isinstance(values, list): - raise DumbMoneyAuthorityError(f"{field} must be an array") - normalized = tuple(str(value) for value in values) - if ( - not normalized - or any(not IDENTIFIER_PATTERN.fullmatch(value) for value in normalized) - or normalized != tuple(sorted(set(normalized))) - ): - raise DumbMoneyAuthorityError( - f"{field} must be nonempty, canonical, sorted, and duplicate-free" - ) - return normalized - - -def _capital_envelope( - payload: dict[str, Any], - expected_account_hash: str, -) -> CapitalEnvelopeV1: - _exact_fields(payload, CAPITAL_ENVELOPE_FIELDS, "CapitalEnvelopeV1") - if payload.get("schema") != CAPITAL_ENVELOPE_SCHEMA: - raise DumbMoneyAuthorityError("CapitalEnvelopeV1 schema is unsupported") - if payload.get("venue") != DOPEY_VENUE: - raise DumbMoneyAuthorityError("CapitalEnvelopeV1 is not for dopey_robinhood") - if payload.get("authorized_mode") != "LIVE": - raise DumbMoneyAuthorityError("CapitalEnvelopeV1 does not authorize LIVE mode") - envelope_id = _sha256_text(payload.get("envelope_id"), "envelope_id") - mandate_id = _sha256_text(payload.get("mandate_id"), "mandate_id") - account_digest = _sha256_text(payload.get("account_hash"), "account_hash") - if account_digest != expected_account_hash: - raise DumbMoneyAuthorityError("CapitalEnvelopeV1 targets a different account") - max_order = _positive_int(payload, "max_order_risk_cents") - max_open = _positive_int(payload, "max_open_risk_cents") - max_correlated = _positive_int(payload, "max_correlated_risk_cents") - max_daily_loss = _positive_int(payload, "max_daily_loss_cents") - if not max_order <= max_correlated <= max_open: - raise DumbMoneyAuthorityError( - "CapitalEnvelopeV1 requires max_order_risk_cents <= " - "max_correlated_risk_cents <= max_open_risk_cents" - ) - not_before = str(payload.get("not_before", "")) - expires_at = str(payload.get("expires_at", "")) - if _utc_timestamp(expires_at, "body.expires_at") <= _utc_timestamp( - not_before, "body.not_before" - ): - raise DumbMoneyAuthorityError("CapitalEnvelopeV1 validity interval is empty") - authorized_instruments = _identifier_array( - payload, - "authorized_instruments", - ) - unsupported_instruments = sorted( - instrument_id - for instrument_id in authorized_instruments - if not ( - EQUITY_INSTRUMENT_ID_PATTERN.fullmatch(instrument_id) - or OPTION_INSTRUMENT_ID_PATTERN.fullmatch(instrument_id) - ) - ) - if unsupported_instruments: - raise DumbMoneyAuthorityError( - "CapitalEnvelopeV1 contains noncanonical exact Dopey instrument IDs: " - f"{unsupported_instruments}" - ) - strategy_hashes = _hash_array(payload, "strategy_hashes") - passport_hashes = _hash_array(payload, "passport_hashes") - promotion_hashes = _hash_array(payload, "promotion_hashes") - if ( - len(strategy_hashes) != 1 - or len(passport_hashes) != 1 - or len(promotion_hashes) != 1 - ): - raise DumbMoneyAuthorityError( - "CapitalEnvelopeV1 must authorize exactly one bound " - "strategy/passport/promotion tuple" - ) - return CapitalEnvelopeV1( - schema=CAPITAL_ENVELOPE_SCHEMA, - envelope_id=envelope_id, - mandate_id=mandate_id, - venue=DOPEY_VENUE, - account_hash=account_digest, - strategy_hashes=strategy_hashes, - passport_hashes=passport_hashes, - promotion_hashes=promotion_hashes, - authorized_instruments=authorized_instruments, - authorized_mode="LIVE", - max_order_risk_cents=max_order, - max_open_risk_cents=max_open, - max_correlated_risk_cents=max_correlated, - max_daily_loss_cents=max_daily_loss, - max_open_orders=_positive_int(payload, "max_open_orders"), - fencing_generation=_positive_int(payload, "fencing_generation"), - policy_epoch=_positive_int(payload, "policy_epoch"), - not_before=not_before, - expires_at=expires_at, - ) - - -@dataclass(frozen=True) -class _VerifiedSignedWrapper: - body_schema: str - body: dict[str, Any] - event_id: str - source_id: str - source_sequence: int - signer_key_id: str - body_digest: str - not_before: str - expires_at: str - not_before_utc: datetime - expires_at_utc: datetime - - -def _verify_signed_wrapper( - payload: dict[str, Any], - trusted_public_key: bytes, - now_utc: datetime, - *, - enforce_current_window: bool, -) -> _VerifiedSignedWrapper: - if now_utc.tzinfo is None: - raise DumbMoneyAuthorityError("Envelope verification requires aware now_utc") - now = now_utc.astimezone(timezone.utc) - _exact_fields(payload, SIGNED_ENVELOPE_FIELDS, "SignedEnvelopeV1") - if payload.get("schema") != SIGNED_ENVELOPE_SCHEMA: - raise DumbMoneyAuthorityError("SignedEnvelopeV1 schema is unsupported") - if payload.get("signature_algorithm") != SIGNATURE_ALGORITHM: - raise DumbMoneyAuthorityError( - "SignedEnvelopeV1 signature algorithm is unsupported" - ) - source_sequence = _positive_int(payload, "source_sequence") - for field in ("source_id", "correlation_id", "nonce"): - if not IDENTIFIER_PATTERN.fullmatch(str(payload.get(field, ""))): - raise DumbMoneyAuthorityError(f"{field} is not a canonical identifier") - causation_id = payload.get("causation_id") - if causation_id is not None: - _sha256_text(causation_id, "causation_id") - body = payload.get("body") - if not isinstance(body, dict) or not all(isinstance(key, str) for key in body): - raise DumbMoneyAuthorityError("SignedEnvelopeV1 body must be an object") - body_schema = str(payload.get("body_schema", "")) - if not IDENTIFIER_PATTERN.fullmatch(body_schema): - raise DumbMoneyAuthorityError( - "SignedEnvelopeV1 body_schema is not a canonical identifier" - ) - if body.get("schema") != body_schema: - raise DumbMoneyAuthorityError( - "SignedEnvelopeV1 body_schema does not match embedded body" - ) - body_digest = hashlib.sha256(canonical_json_bytes(body)).hexdigest() - if payload.get("body_digest") != body_digest: - raise DumbMoneyAuthorityError("SignedEnvelopeV1 body digest mismatch") - unsigned_event = dict(payload) - unsigned_event.pop("signature", None) - unsigned_event.pop("event_id", None) - event_id = hashlib.sha256(canonical_json_bytes(unsigned_event)).hexdigest() - if payload.get("event_id") != event_id: - raise DumbMoneyAuthorityError("SignedEnvelopeV1 event_id mismatch") - public_key = _decode_public_key(trusted_public_key) - signer_key_id = hashlib.sha256(public_key).hexdigest() - if payload.get("signer_key_id") != signer_key_id: - raise DumbMoneyAuthorityError("SignedEnvelopeV1 signer_key_id mismatch") - try: - signature = _urlsafe_b64decode(str(payload.get("signature", ""))) - if len(signature) != 64: - raise ValueError("Ed25519 signature must be 64 bytes") - Ed25519PublicKey.from_public_bytes(public_key).verify( - signature, - canonical_json_bytes( - {key: value for key, value in payload.items() if key != "signature"} - ), - ) - except (InvalidSignature, ValueError) as error: - raise DumbMoneyAuthorityError( - "SignedEnvelopeV1 signature is invalid" - ) from error - not_before = str(payload.get("not_before", "")) - expires_at = str(payload.get("expires_at", "")) - wrapper_not_before = _utc_timestamp(not_before, "not_before") - wrapper_expires = _utc_timestamp(expires_at, "expires_at") - if wrapper_expires <= wrapper_not_before: - raise DumbMoneyAuthorityError("SignedEnvelopeV1 validity interval is empty") - if enforce_current_window and not wrapper_not_before <= now < wrapper_expires: - raise DumbMoneyAuthorityError("SignedEnvelopeV1 is not currently valid") - return _VerifiedSignedWrapper( - body_schema=body_schema, - body=dict(body), - event_id=event_id, - source_id=str(payload["source_id"]), - source_sequence=source_sequence, - signer_key_id=signer_key_id, - body_digest=body_digest, - not_before=not_before, - expires_at=expires_at, - not_before_utc=wrapper_not_before, - expires_at_utc=wrapper_expires, - ) - - -def _verified_capital_from_wrapper( - wrapper: _VerifiedSignedWrapper, - now_utc: datetime, - expected_account_hash: str, - *, - enforce_current_window: bool, -) -> VerifiedCapitalAuthority: - if wrapper.body_schema != CAPITAL_ENVELOPE_SCHEMA: - raise DumbMoneyAuthorityError( - "SignedEnvelopeV1 body_schema is not capital authority" - ) - now = now_utc.astimezone(timezone.utc) - envelope = _capital_envelope(wrapper.body, expected_account_hash) - body_not_before = _utc_timestamp(envelope.not_before, "body.not_before") - body_expires = _utc_timestamp(envelope.expires_at, "body.expires_at") - if enforce_current_window and not body_not_before <= now < body_expires: - raise DumbMoneyAuthorityError("CapitalEnvelopeV1 is not currently valid") - if ( - wrapper.not_before_utc != body_not_before - or wrapper.expires_at_utc != body_expires - ): - raise DumbMoneyAuthorityError( - "SignedEnvelopeV1 and CapitalEnvelopeV1 validity windows must match exactly" - ) - if wrapper.expires_at_utc - wrapper.not_before_utc > MAXIMUM_CAPITAL_ENVELOPE_TTL: - raise DumbMoneyAuthorityError( - "Capital authority validity exceeds the local 60-second maximum" - ) - return VerifiedCapitalAuthority( - envelope=envelope, - event_id=wrapper.event_id, - source_id=wrapper.source_id, - source_sequence=wrapper.source_sequence, - signer_key_id=wrapper.signer_key_id, - body_digest=wrapper.body_digest, - ) - - -def verify_signed_capital_envelope( - payload: dict[str, Any], - trusted_public_key: bytes, - now_utc: datetime, - expected_account_hash: str, -) -> VerifiedCapitalAuthority: - wrapper = _verify_signed_wrapper( - payload, - trusted_public_key, - now_utc, - enforce_current_window=True, - ) - return _verified_capital_from_wrapper( - wrapper, - now_utc, - expected_account_hash, - enforce_current_window=True, - ) - - -def _desired_mode( - payload: dict[str, Any], - now_utc: datetime, - *, - enforce_current_window: bool, -) -> DesiredModeV1: - _exact_fields(payload, DESIRED_MODE_FIELDS, "DesiredModeV1") - if payload.get("schema") != DESIRED_MODE_SCHEMA: - raise DumbMoneyAuthorityError("DesiredModeV1 schema is unsupported") - if payload.get("venue") != DOPEY_VENUE: - raise DumbMoneyAuthorityError("DesiredModeV1 is not for dopey_robinhood") - mode = str(payload.get("mode", "")) - if mode not in DESIRED_MODES: - raise DumbMoneyAuthorityError("DesiredModeV1 mode is unsupported") - reason = payload.get("reason") - if not isinstance(reason, str) or not reason.strip(): - raise DumbMoneyAuthorityError("DesiredModeV1 reason must be nonempty") - not_before = str(payload.get("not_before", "")) - expires_at = str(payload.get("expires_at", "")) - body_not_before = _utc_timestamp(not_before, "body.not_before") - body_expires = _utc_timestamp(expires_at, "body.expires_at") - if body_expires <= body_not_before: - raise DumbMoneyAuthorityError("DesiredModeV1 validity interval is empty") - now = now_utc.astimezone(timezone.utc) - if enforce_current_window and not body_not_before <= now < body_expires: - raise DumbMoneyAuthorityError("DesiredModeV1 is not currently valid") - return DesiredModeV1( - schema=DESIRED_MODE_SCHEMA, - venue=DOPEY_VENUE, - mode=mode, - revision=_positive_int(payload, "revision"), - reason=reason, - policy_epoch=_positive_int(payload, "policy_epoch"), - not_before=not_before, - expires_at=expires_at, - ) - - -def _kill_state( - payload: dict[str, Any], - now_utc: datetime, - *, - enforce_current_window: bool, -) -> KillStateV1: - _exact_fields(payload, KILL_STATE_FIELDS, "KillStateV1") - if payload.get("schema") != KILL_STATE_SCHEMA: - raise DumbMoneyAuthorityError("KillStateV1 schema is unsupported") - active = payload.get("active") - if not isinstance(active, bool): - raise DumbMoneyAuthorityError("KillStateV1 active must be a boolean") - reason = payload.get("reason") - if not isinstance(reason, str) or not reason.strip(): - raise DumbMoneyAuthorityError("KillStateV1 reason must be nonempty") - changed_by = str(payload.get("changed_by", "")) - if not IDENTIFIER_PATTERN.fullmatch(changed_by): - raise DumbMoneyAuthorityError( - "KillStateV1 changed_by is not a canonical identifier" - ) - changed_at = str(payload.get("changed_at", "")) - changed_at_utc = _utc_timestamp(changed_at, "body.changed_at") - if enforce_current_window and changed_at_utc > now_utc.astimezone(timezone.utc): - raise DumbMoneyAuthorityError("KillStateV1 changed_at is future-dated") - return KillStateV1( - schema=KILL_STATE_SCHEMA, - active=active, - generation=_positive_int(payload, "generation"), - reason=reason, - changed_by=changed_by, - policy_epoch=_positive_int(payload, "policy_epoch"), - changed_at=changed_at, - ) - - -def verify_signed_control_envelope( - payload: dict[str, Any], - trusted_public_key: bytes, - now_utc: datetime, - expected_account_hash: str, - *, - enforce_current_window: bool = True, -) -> VerifiedControlEnvelope: - """Verify one Core command. - - ``enforce_current_window=False`` exists only so the command-feed recovery - path can authenticate previously persisted generations. It must never be - used to grant positive capital authority. - """ - - wrapper = _verify_signed_wrapper( - payload, - trusted_public_key, - now_utc, - enforce_current_window=enforce_current_window, - ) - if wrapper.body_schema == CAPITAL_ENVELOPE_SCHEMA: - return _verified_capital_from_wrapper( - wrapper, - now_utc, - expected_account_hash, - enforce_current_window=enforce_current_window, - ) - if wrapper.body_schema == DESIRED_MODE_SCHEMA: - desired = _desired_mode( - wrapper.body, - now_utc, - enforce_current_window=enforce_current_window, - ) - return VerifiedDesiredMode( - desired_mode=desired, - event_id=wrapper.event_id, - source_id=wrapper.source_id, - source_sequence=wrapper.source_sequence, - signer_key_id=wrapper.signer_key_id, - body_digest=wrapper.body_digest, - not_before=wrapper.not_before, - expires_at=wrapper.expires_at, - ) - if wrapper.body_schema == KILL_STATE_SCHEMA: - kill = _kill_state( - wrapper.body, - now_utc, - enforce_current_window=enforce_current_window, - ) - return VerifiedKillState( - kill_state=kill, - event_id=wrapper.event_id, - source_id=wrapper.source_id, - source_sequence=wrapper.source_sequence, - signer_key_id=wrapper.signer_key_id, - body_digest=wrapper.body_digest, - not_before=wrapper.not_before, - expires_at=wrapper.expires_at, - ) - raise DumbMoneyAuthorityError( - f"SignedEnvelopeV1 body_schema is unsupported: {wrapper.body_schema}" - ) - - -def account_open_risk_cents(account: AccountSnapshot) -> int: - total_usd = 0.0 - for multiplier, positions in ( - (1.0, account.equity_positions), - (100.0, account.option_positions), - ): - for position in positions: - required = {"quantity", "average_cost", "market_value"} - if not required <= set(position): - raise DumbMoneyAuthorityError( - "Nonzero broker position lacks complete finite risk evidence" - ) - try: - market_value = abs(float(position.get("market_value", 0.0))) - quantity = abs(float(position.get("quantity", 0.0))) - average_cost = abs(float(position.get("average_cost", 0.0))) - except (TypeError, ValueError) as error: - raise DumbMoneyAuthorityError( - "Broker position risk fields are not numeric" - ) from error - cost_basis = quantity * average_cost * multiplier - if not math.isfinite(market_value) or not math.isfinite(cost_basis): - raise DumbMoneyAuthorityError("Broker position cost basis is invalid") - position_risk = max(market_value, cost_basis) - if quantity > 0.0 and position_risk <= 0.0: - raise DumbMoneyAuthorityError( - "Nonzero broker position lacks positive risk evidence" - ) - total_usd += position_risk - if not math.isfinite(total_usd): - raise DumbMoneyAuthorityError("Broker open risk is not finite") - return math.ceil(total_usd * 100.0 - 1e-9) - - -def active_order_count(account: AccountSnapshot) -> int: - active = {"queued", "pending", "open", "confirmed", "partially_filled"} - return sum( - str(order.get("status", "")).strip().lower() in active - for order in (*account.recent_equity_orders, *account.recent_option_orders) - ) - - -def active_entry_order_present(account: AccountSnapshot) -> bool: - active = {"queued", "pending", "open", "confirmed", "partially_filled"} - return any( - str(order.get("status", "")).strip().lower() in active - and str(order.get("side", "")).strip().lower() == "buy" - for order in (*account.recent_equity_orders, *account.recent_option_orders) - ) - - -def _validated_daily_loss( - authority: VerifiedCapitalAuthority, - observed_daily_loss_cents: int | None, -) -> int: - if ( - isinstance(observed_daily_loss_cents, bool) - or not isinstance(observed_daily_loss_cents, int) - or observed_daily_loss_cents < 0 - ): - raise DumbMoneyAuthorityError( - "Broker/ledger-witnessed daily loss is required for new exposure" - ) - if observed_daily_loss_cents >= authority.envelope.max_daily_loss_cents: - raise DumbMoneyAuthorityError( - "CapitalEnvelopeV1 max_daily_loss_cents is exhausted" - ) - return observed_daily_loss_cents - - -def authorized_settled_cash_usd( - authority: VerifiedCapitalAuthority, - account: AccountSnapshot, - spendable_settled_usd: float, - observed_daily_loss_cents: int | None, -) -> float: - if not math.isfinite(spendable_settled_usd) or spendable_settled_usd < 0.0: - raise DumbMoneyAuthorityError("Settled cash must be finite and nonnegative") - _validated_daily_loss(authority, observed_daily_loss_cents) - open_risk = account_open_risk_cents(account) - ceiling = min( - authority.envelope.max_open_risk_cents, - authority.envelope.max_correlated_risk_cents, - ) - remaining_cents = max(0, ceiling - open_risk) - return min(spendable_settled_usd, remaining_cents / 100.0) - - -def intent_incremental_risk_cents(intent: LiveExecutionIntent) -> int: - if intent.side == "sell": - return 0 - if intent.instrument == "option": - contract = intent.option_contract - multiplier = contract.get("multiplier", 100) - try: - maximum_loss = float(contract.get("maximum_loss_usd", 0.0)) - computed_debit = ( - float(intent.limit_price) * float(intent.quantity) * float(multiplier) - ) - except (TypeError, ValueError) as error: - raise DumbMoneyAuthorityError( - "Option debit fields are not numeric" - ) from error - risk_usd = max(maximum_loss, computed_debit, intent.notional_usd) - else: - risk_usd = max(intent.notional_usd, intent.quantity * intent.limit_price) - if not math.isfinite(risk_usd) or risk_usd <= 0.0: - raise DumbMoneyAuthorityError("Entry intent risk must be finite and positive") - return math.ceil(risk_usd * 100.0 - 1e-9) - - -def intent_authorized_instrument_id(intent: LiveExecutionIntent) -> str: - """Return the exact broker-sink identity used by CapitalEnvelopeV1.""" - - symbol = intent.symbol.strip().upper() - if intent.instrument == "equity": - instrument_id = f"equity:{symbol}" - if not EQUITY_INSTRUMENT_ID_PATTERN.fullmatch(instrument_id): - raise DumbMoneyAuthorityError( - "Equity intent cannot form a canonical authorized instrument ID" - ) - return instrument_id - if intent.instrument == "option": - contract_id = str(intent.option_contract.get("contract_id", "")).strip() - instrument_id = f"option:{symbol}:{contract_id}" - if not OPTION_INSTRUMENT_ID_PATTERN.fullmatch(instrument_id): - raise DumbMoneyAuthorityError( - "Option intent cannot form a canonical authorized instrument ID" - ) - return instrument_id - raise DumbMoneyAuthorityError( - f"Unsupported Dopey execution instrument: {intent.instrument}" - ) - - -def authorize_intent( - authority: VerifiedCapitalAuthority, - account: AccountSnapshot, - intent: LiveExecutionIntent, - observed_daily_loss_cents: int | None, -) -> int: - if intent.side == "sell": - return _authorize_reducing_exit(account, intent) - witnessed_daily_loss = _validated_daily_loss( - authority, - observed_daily_loss_cents, - ) - if active_entry_order_present(account): - raise DumbMoneyAuthorityError( - "Existing active entry order blocks additional new exposure" - ) - instrument_id = intent_authorized_instrument_id(intent) - if instrument_id not in authority.envelope.authorized_instruments: - raise DumbMoneyAuthorityError( - "Execution intent exact broker instrument is absent from " - "CapitalEnvelopeV1 authorized_instruments" - ) - source_hash = _sha256_text(intent.source_hash, "intent.source_hash") - if source_hash != authority.envelope.strategy_hashes[0]: - raise DumbMoneyAuthorityError( - "Execution intent strategy hash does not match the exact " - "CapitalEnvelopeV1 strategy" - ) - passport_hash = _sha256_text( - intent.passport_hash, - "intent.passport_hash", - ) - if passport_hash != authority.envelope.passport_hashes[0]: - raise DumbMoneyAuthorityError( - "Execution intent passport hash does not match the exact " - "CapitalEnvelopeV1 passport" - ) - promotion_hash = _sha256_text( - intent.promotion_hash, - "intent.promotion_hash", - ) - if promotion_hash != authority.envelope.promotion_hashes[0]: - raise DumbMoneyAuthorityError( - "Execution intent promotion hash does not match the exact " - "CapitalEnvelopeV1 promotion" - ) - incremental = intent_incremental_risk_cents(intent) - if incremental > authority.envelope.max_order_risk_cents: - raise DumbMoneyAuthorityError("Execution intent exceeds max_order_risk_cents") - remaining_daily_loss_capacity = ( - authority.envelope.max_daily_loss_cents - witnessed_daily_loss - ) - if incremental > remaining_daily_loss_capacity: - raise DumbMoneyAuthorityError( - "Execution intent worst-case risk exceeds remaining daily-loss capacity" - ) - if active_order_count(account) >= authority.envelope.max_open_orders: - raise DumbMoneyAuthorityError("CapitalEnvelopeV1 max_open_orders is exhausted") - projected = account_open_risk_cents(account) + incremental - if projected > authority.envelope.max_open_risk_cents: - raise DumbMoneyAuthorityError("Execution intent exceeds max_open_risk_cents") - if projected > authority.envelope.max_correlated_risk_cents: - raise DumbMoneyAuthorityError( - "Execution intent exceeds max_correlated_risk_cents" - ) - return incremental - - -def _authorize_reducing_exit( - account: AccountSnapshot, - intent: LiveExecutionIntent, -) -> int: - try: - requested_quantity = float(intent.quantity) - except (TypeError, ValueError) as error: - raise DumbMoneyAuthorityError("Exit intent quantity is not numeric") from error - if not math.isfinite(requested_quantity) or requested_quantity <= 0.0: - raise DumbMoneyAuthorityError( - "Exit intent quantity must be finite and positive" - ) - if intent.instrument == "equity": - positions = tuple( - position - for position in account.equity_positions - if str(position.get("symbol", "")).strip().upper() - == intent.symbol.strip().upper() - ) - matching_active_sells = tuple( - order - for order in account.recent_equity_orders - if str(order.get("symbol", "")).strip().upper() - == intent.symbol.strip().upper() - and str(order.get("side", "")).strip().lower() == "sell" - and str(order.get("status", "")).strip().lower() - in {"queued", "pending", "open", "confirmed", "partially_filled"} - ) - elif intent.instrument == "option": - contract_id = str(intent.option_contract.get("contract_id", "")).strip() - if not contract_id: - raise DumbMoneyAuthorityError( - "Option exit intent lacks an exact contract_id" - ) - positions = tuple( - position - for position in account.option_positions - if str(position.get("contract_id", "")).strip() == contract_id - ) - matching_active_sells = tuple( - order - for order in account.recent_option_orders - if str(order.get("contract_id", "")).strip() == contract_id - and str(order.get("side", "")).strip().lower() == "sell" - and str(order.get("status", "")).strip().lower() - in {"queued", "pending", "open", "confirmed", "partially_filled"} - ) - else: - raise DumbMoneyAuthorityError( - f"Unsupported exit instrument: {intent.instrument}" - ) - if len(positions) != 1: - raise DumbMoneyAuthorityError( - "Exit intent requires exactly one fresh matching long broker position" - ) - if matching_active_sells: - raise DumbMoneyAuthorityError( - "Existing active sell order blocks another exit for the same holding" - ) - try: - held_quantity = float(positions[0]["quantity"]) - except (KeyError, TypeError, ValueError) as error: - raise DumbMoneyAuthorityError( - "Matching broker position lacks numeric quantity evidence" - ) from error - if not math.isfinite(held_quantity) or held_quantity <= 0.0: - raise DumbMoneyAuthorityError( - "Matching broker position quantity must be finite and positive" - ) - if requested_quantity > held_quantity + 1e-9: - raise DumbMoneyAuthorityError( - "Exit intent would oversell the fresh witnessed long position" - ) - return 0 diff --git a/dopey_live/dumbmoney_command_feed.py b/dopey_live/dumbmoney_command_feed.py deleted file mode 100644 index e51fb01..0000000 --- a/dopey_live/dumbmoney_command_feed.py +++ /dev/null @@ -1,1596 +0,0 @@ -"""Authenticated, pull-only DumbMoney Core command delivery for DOPEY. - -Importing this module starts no thread, listener, timer, or network request. -The supervising process must inject both the cell bearer-token provider and a -single-request transport into :func:`poll_once`. -""" - -from __future__ import annotations - -import hashlib -import json -import math -import os -import secrets -import tempfile -from contextlib import contextmanager -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import Any, Callable, Final, Iterator, Mapping, Protocol -from urllib.parse import urlencode, urlsplit - -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey - -from .dumbmoney_authority import ( - CAPITAL_ENVELOPE_SCHEMA, - DESIRED_MODE_SCHEMA, - DOPEY_VENUE, - KILL_STATE_SCHEMA, - DumbMoneyAuthorityContext, - DumbMoneyAuthorityError, - VerifiedCapitalAuthority, - VerifiedDesiredMode, - VerifiedKillState, - _decode_public_key, - _sha256_text, - _urlsafe_b64decode, - _utc_timestamp, - canonical_json_bytes, - verify_signed_control_envelope, -) -from .ledger import artifact_root - -COMMAND_PAGE_SCHEMA: Final[str] = "dumbmoney.cell-command-page.v1" -COMMAND_CHECKPOINT_SCHEMA: Final[str] = "dumbmoney.cell-command-checkpoint.v1" -LEDGER_EVENT_PROOF_SCHEMA: Final[str] = "dumbmoney.ledger-event-proof.v1" -COMMAND_STATE_SCHEMA: Final[str] = "dopey.dumbmoney-command-feed-state.v1" -GENESIS_DIGEST: Final[str] = "0" * 64 -DEFAULT_PAGE_LIMIT: Final[int] = 250 -MAXIMUM_PAGE_LIMIT: Final[int] = 1000 -MAXIMUM_RESPONSE_BYTES: Final[int] = 1_048_576 -MAXIMUM_PAGE_CLOCK_SKEW: Final[timedelta] = timedelta(seconds=60) -MAXIMUM_KILL_CHANGED_AT_FUTURE_SKEW: Final[timedelta] = timedelta(seconds=5) -VALID_REQUIRED_ACTIONS: Final[frozenset[str]] = frozenset( - {"APPLY_SIGNED_CONTROLS", "PAUSE_NEW_RISK", "CANCEL_AND_RECONCILE"} -) -COMMAND_PAGE_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "cell_id", - "request_nonce", - "observed_at", - "after_sequence", - "after_digest", - "next_sequence", - "next_digest", - "ledger_head_sequence", - "ledger_head_digest", - "has_more", - "required_action", - "commands", - "checkpoint", - "checkpoint_signature", - } -) -COMMAND_FIELDS: Final[frozenset[str]] = frozenset( - { - "global_sequence", - "event_id", - "event_digest", - "body_schema", - "valid_now", - "transport_window_current", - "authority_effect", - "ledger_proof", - "envelope", - } -) -ORDERED_COMMAND_FIELDS: Final[frozenset[str]] = COMMAND_FIELDS - {"envelope"} -COMMAND_CHECKPOINT_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "cell_id", - "request_nonce", - "after_sequence", - "after_digest", - "ordered_commands", - "next_sequence", - "next_digest", - "ledger_head_sequence", - "ledger_head_digest", - "observed_at", - "required_action", - } -) -CHECKPOINT_SIGNATURE_FIELDS: Final[frozenset[str]] = frozenset( - {"algorithm", "signer_key_id", "signature"} -) -LEDGER_EVENT_PROOF_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "global_sequence", - "event_id", - "source_id", - "source_sequence", - "signer_key_id", - "nonce", - "event_schema", - "observed_at", - "received_at", - "correlation_id", - "causation_id", - "payload_digest", - "previous_source_digest", - "previous_global_digest", - "event_digest", - } -) -VALID_AUTHORITY_EFFECTS: Final[frozenset[str]] = frozenset( - {"APPLY_FAIL_CLOSED", "APPLY_POSITIVE", "HISTORICAL_ONLY"} -) -COMMAND_STATE_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "cell_id", - "next_sequence", - "next_digest", - "kill_envelope", - "desired_mode_envelope", - "capital_envelope", - "updated_at", - } -) - - -class DumbMoneyCommandFeedError(RuntimeError): - """Raised internally when the command feed cannot advance safely.""" - - -@dataclass(frozen=True) -class CommandFeedHttpRequest: - method: str - url: str - headers: Mapping[str, str] - timeout_seconds: float - body: bytes | None = None - - -@dataclass(frozen=True) -class CommandFeedHttpResponse: - status: int - headers: Mapping[str, str] - body: bytes - - -class CommandFeedTransport(Protocol): - def __call__( - self, - request: CommandFeedHttpRequest, - ) -> CommandFeedHttpResponse: ... - - -@dataclass(frozen=True) -class DumbMoneyCommandFeedClient: - """Injected, side-effect-free-until-called command-feed binding.""" - - base_url: str - cell_bearer_provider: Callable[[], str] - trusted_public_key: bytes - trusted_operator_public_keys: Mapping[str, bytes] - expected_account_hash: str - state_path: Path - transport: CommandFeedTransport - limit: int = DEFAULT_PAGE_LIMIT - timeout_seconds: float = 5.0 - - def poll_once(self, now_utc: datetime) -> DumbMoneyCommandDispatch: - return poll_once( - base_url=self.base_url, - cell_bearer_provider=self.cell_bearer_provider, - trusted_public_key=self.trusted_public_key, - trusted_operator_public_keys=self.trusted_operator_public_keys, - expected_account_hash=self.expected_account_hash, - state_path=self.state_path, - transport=self.transport, - now_utc=now_utc, - limit=self.limit, - timeout_seconds=self.timeout_seconds, - ) - - -@dataclass(frozen=True) -class DumbMoneyCommandDispatch: - """Controls safe to hand to one Live Branch cycle.""" - - desired_mode: str - kill_active: bool - allow_new_risk: bool - capital_authority: DumbMoneyAuthorityContext | None - fail_closed_reason: str - required_action: str - caught_up: bool - next_sequence: int - next_digest: str - applied_event_ids: tuple[str, ...] - - def evidence(self) -> dict[str, Any]: - return { - "desired_mode": self.desired_mode, - "kill_active": self.kill_active, - "allow_new_risk": self.allow_new_risk, - "capital_authority_present": self.capital_authority is not None, - "fail_closed_reason": self.fail_closed_reason, - "required_action": self.required_action, - "caught_up": self.caught_up, - "next_sequence": self.next_sequence, - "next_digest": self.next_digest, - "applied_event_ids": list(self.applied_event_ids), - } - - -@dataclass(frozen=True) -class _CommandState: - next_sequence: int = 0 - next_digest: str = GENESIS_DIGEST - kill_envelope: dict[str, Any] | None = None - desired_mode_envelope: dict[str, Any] | None = None - capital_envelope: dict[str, Any] | None = None - updated_at: str = "1970-01-01T00:00:00Z" - - def to_dict(self) -> dict[str, Any]: - return { - "schema": COMMAND_STATE_SCHEMA, - "cell_id": DOPEY_VENUE, - "next_sequence": self.next_sequence, - "next_digest": self.next_digest, - "kill_envelope": self.kill_envelope, - "desired_mode_envelope": self.desired_mode_envelope, - "capital_envelope": self.capital_envelope, - "updated_at": self.updated_at, - } - - -def command_feed_state_path(repository_root: Path) -> Path: - return artifact_root(repository_root) / "dumbmoney_command_feed_state.json" - - -def command_feed_head(state_path: Path) -> tuple[int, str]: - """Return one integrity-checked persisted cursor for Core anchoring.""" - - state = _read_state(state_path.resolve()) - return state.next_sequence, state.next_digest - - -def _format_utc(value: datetime) -> str: - utc = value.astimezone(timezone.utc) - rendered = utc.isoformat(timespec="microseconds" if utc.microsecond else "seconds") - return rendered.replace("+00:00", "Z") - - -def _positive_int(value: Any, label: str) -> int: - if isinstance(value, bool) or not isinstance(value, int) or value <= 0: - raise DumbMoneyCommandFeedError(f"{label} must be a positive integer") - return value - - -def _nonnegative_int(value: Any, label: str) -> int: - if isinstance(value, bool) or not isinstance(value, int) or value < 0: - raise DumbMoneyCommandFeedError(f"{label} must be a nonnegative integer") - return value - - -def _digest(value: Any, label: str) -> str: - try: - return _sha256_text(value, label) - except DumbMoneyAuthorityError as error: - raise DumbMoneyCommandFeedError(str(error)) from error - - -def _trusted_keyring( - trusted_public_key: bytes, - trusted_operator_public_keys: Mapping[str, bytes], -) -> tuple[bytes, dict[str, bytes]]: - try: - core_key = _decode_public_key(trusted_public_key) - except DumbMoneyAuthorityError as error: - raise DumbMoneyCommandFeedError("pinned Core public key is invalid") from error - core_key_id = hashlib.sha256(core_key).hexdigest() - if ( - not isinstance(trusted_operator_public_keys, Mapping) - or not 1 <= len(trusted_operator_public_keys) <= 16 - ): - raise DumbMoneyCommandFeedError( - "trusted operator public-key set must contain 1 to 16 keys" - ) - operator_keys: dict[str, bytes] = {} - for raw_key_id, raw_key in trusted_operator_public_keys.items(): - key_id = _digest(raw_key_id, "trusted operator key ID") - try: - public_key = _decode_public_key(raw_key) - except DumbMoneyAuthorityError as error: - raise DumbMoneyCommandFeedError( - f"trusted operator public key is invalid: key_id={key_id}" - ) from error - if hashlib.sha256(public_key).hexdigest() != key_id: - raise DumbMoneyCommandFeedError( - f"trusted operator key ID does not bind its key: key_id={key_id}" - ) - if key_id == core_key_id: - raise DumbMoneyCommandFeedError( - "Core checkpoint key and operator command keys must be disjoint" - ) - operator_keys[key_id] = public_key - return core_key, operator_keys - - -def _command_public_key( - envelope: Mapping[str, Any], - body_schema: str, - core_public_key: bytes, - operator_public_keys: Mapping[str, bytes], -) -> bytes: - signer_key_id = _digest( - envelope.get("signer_key_id"), - "command signer_key_id", - ) - core_key_id = hashlib.sha256(core_public_key).hexdigest() - if body_schema == CAPITAL_ENVELOPE_SCHEMA: - if signer_key_id != core_key_id: - raise DumbMoneyCommandFeedError( - "CapitalEnvelopeV1 command is not signed by pinned Core" - ) - return core_public_key - if body_schema in {DESIRED_MODE_SCHEMA, KILL_STATE_SCHEMA}: - public_key = operator_public_keys.get(signer_key_id) - if public_key is None: - raise DumbMoneyCommandFeedError( - "operator control command signer is outside the sealed operator key set" - ) - return public_key - raise DumbMoneyCommandFeedError( - f"command body schema is unsupported: {body_schema}" - ) - - -def _verify_checkpoint_signature( - checkpoint: Mapping[str, Any], - signature_payload: Mapping[str, Any], - core_public_key: bytes, -) -> None: - _exact_fields( - checkpoint, - COMMAND_CHECKPOINT_FIELDS, - "command checkpoint", - ) - _exact_fields( - signature_payload, - CHECKPOINT_SIGNATURE_FIELDS, - "command checkpoint signature", - ) - if checkpoint.get("schema") != COMMAND_CHECKPOINT_SCHEMA: - raise DumbMoneyCommandFeedError("command checkpoint schema is unsupported") - if signature_payload.get("algorithm") != "Ed25519": - raise DumbMoneyCommandFeedError( - "command checkpoint signature algorithm is unsupported" - ) - expected_key_id = hashlib.sha256(core_public_key).hexdigest() - if signature_payload.get("signer_key_id") != expected_key_id: - raise DumbMoneyCommandFeedError("command checkpoint signer is not pinned Core") - try: - signature = _urlsafe_b64decode(str(signature_payload.get("signature", ""))) - if len(signature) != 64: - raise ValueError("Ed25519 signature must be 64 bytes") - Ed25519PublicKey.from_public_bytes(core_public_key).verify( - signature, - canonical_json_bytes(dict(checkpoint)), - ) - except (InvalidSignature, ValueError) as error: - raise DumbMoneyCommandFeedError( - "command checkpoint signature is invalid" - ) from error - - -def _exact_fields( - payload: Mapping[str, Any], - expected: frozenset[str], - label: str, -) -> None: - missing = sorted(expected - set(payload)) - unknown = sorted(set(payload) - expected) - if missing or unknown: - raise DumbMoneyCommandFeedError( - f"{label} schema mismatch: missing={missing}, unknown={unknown}" - ) - - -def _reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: - result: dict[str, Any] = {} - for key, value in pairs: - if key in result: - raise DumbMoneyCommandFeedError(f"duplicate JSON key is prohibited: {key}") - result[key] = value - return result - - -def _reject_json_constant(value: str) -> None: - raise DumbMoneyCommandFeedError(f"non-finite JSON value is prohibited: {value}") - - -def _validate_json_domain(value: Any, path: str = "$") -> None: - if value is None or isinstance(value, (str, bool, int)): - return - if isinstance(value, float): - if not math.isfinite(value): - raise DumbMoneyCommandFeedError(f"{path} contains a non-finite number") - raise DumbMoneyCommandFeedError( - f"{path} contains a float outside the signed integer domain" - ) - if isinstance(value, list): - for index, item in enumerate(value): - _validate_json_domain(item, f"{path}[{index}]") - return - if isinstance(value, dict): - if not all(isinstance(key, str) for key in value): - raise DumbMoneyCommandFeedError(f"{path} contains a non-string object key") - for key, item in value.items(): - _validate_json_domain(item, f"{path}.{key}") - return - raise DumbMoneyCommandFeedError( - f"{path} contains unsupported JSON type {type(value).__name__}" - ) - - -def _loads_strict_json(raw: bytes, label: str) -> dict[str, Any]: - try: - value = json.loads( - raw, - object_pairs_hook=_reject_duplicate_keys, - parse_constant=_reject_json_constant, - ) - except (UnicodeDecodeError, json.JSONDecodeError) as error: - raise DumbMoneyCommandFeedError(f"{label} is not valid JSON") from error - if not isinstance(value, dict): - raise DumbMoneyCommandFeedError(f"{label} root must be an object") - _validate_json_domain(value) - return value - - -def _validated_ledger_proof( - raw_proof: Any, - command: Mapping[str, Any], - envelope: Mapping[str, Any], -) -> dict[str, Any]: - if not isinstance(raw_proof, dict): - raise DumbMoneyCommandFeedError("command ledger_proof must be an object") - _exact_fields(raw_proof, LEDGER_EVENT_PROOF_FIELDS, "ledger event proof") - if raw_proof.get("schema") != LEDGER_EVENT_PROOF_SCHEMA: - raise DumbMoneyCommandFeedError("ledger event proof schema is unsupported") - global_sequence = _positive_int( - raw_proof.get("global_sequence"), - "ledger proof global_sequence", - ) - source_sequence = _positive_int( - raw_proof.get("source_sequence"), - "ledger proof source_sequence", - ) - event_id = _digest(raw_proof.get("event_id"), "ledger proof event_id") - signer_key_id = _digest( - raw_proof.get("signer_key_id"), - "ledger proof signer_key_id", - ) - payload_digest = _digest( - raw_proof.get("payload_digest"), - "ledger proof payload_digest", - ) - _digest( - raw_proof.get("previous_source_digest"), - "ledger proof previous_source_digest", - ) - _digest( - raw_proof.get("previous_global_digest"), - "ledger proof previous_global_digest", - ) - event_digest = _digest( - raw_proof.get("event_digest"), - "ledger proof event_digest", - ) - for field in ("source_id", "nonce", "event_schema"): - value = raw_proof.get(field) - if not isinstance(value, str) or not value: - raise DumbMoneyCommandFeedError(f"ledger proof {field} must be nonempty") - correlation_id = raw_proof.get("correlation_id") - if not isinstance(correlation_id, str) or not correlation_id: - raise DumbMoneyCommandFeedError("ledger proof correlation_id must be nonempty") - causation_id = raw_proof.get("causation_id") - if causation_id is not None: - _digest(causation_id, "ledger proof causation_id") - try: - observed_at = _utc_timestamp( - raw_proof.get("observed_at"), - "ledger proof observed_at", - ) - received_at = _utc_timestamp( - raw_proof.get("received_at"), - "ledger proof received_at", - ) - envelope_not_before = _utc_timestamp( - envelope.get("not_before"), - "ledger proof envelope not_before", - ) - except DumbMoneyAuthorityError as error: - raise DumbMoneyCommandFeedError(str(error)) from error - if observed_at != max(received_at, envelope_not_before): - raise DumbMoneyCommandFeedError( - "ledger proof observed_at does not match ledger receipt semantics" - ) - if (source_sequence == 1) is not ( - raw_proof["previous_source_digest"] == GENESIS_DIGEST - ): - raise DumbMoneyCommandFeedError( - "ledger proof source-chain genesis semantics are invalid" - ) - if (global_sequence == 1) is not ( - raw_proof["previous_global_digest"] == GENESIS_DIGEST - ): - raise DumbMoneyCommandFeedError( - "ledger proof global-chain genesis semantics are invalid" - ) - event_core = { - key: value - for key, value in raw_proof.items() - if key not in {"schema", "event_digest"} - } - recomputed = hashlib.sha256(canonical_json_bytes(event_core)).hexdigest() - if recomputed != event_digest: - raise DumbMoneyCommandFeedError("ledger proof event_digest does not recompute") - duplicate_bindings = ( - (global_sequence, command.get("global_sequence"), "global_sequence"), - (event_id, command.get("event_id"), "event_id"), - (event_digest, command.get("event_digest"), "event_digest"), - (raw_proof["event_schema"], command.get("body_schema"), "body_schema"), - (event_id, envelope.get("event_id"), "envelope event_id"), - (raw_proof["source_id"], envelope.get("source_id"), "source_id"), - ( - source_sequence, - envelope.get("source_sequence"), - "source_sequence", - ), - ( - signer_key_id, - envelope.get("signer_key_id"), - "signer_key_id", - ), - (raw_proof["nonce"], envelope.get("nonce"), "nonce"), - ( - raw_proof["event_schema"], - envelope.get("body_schema"), - "event_schema", - ), - ( - correlation_id, - envelope.get("correlation_id"), - "correlation_id", - ), - (causation_id, envelope.get("causation_id"), "causation_id"), - ( - payload_digest, - envelope.get("body_digest"), - "payload_digest", - ), - ) - for observed, expected, label in duplicate_bindings: - if observed != expected: - raise DumbMoneyCommandFeedError(f"ledger proof {label} binding mismatch") - return dict(raw_proof) - - -def _validate_command_projection( - command: Mapping[str, Any], - verified: VerifiedCapitalAuthority | VerifiedDesiredMode | VerifiedKillState, - now_utc: datetime, -) -> None: - transport_current = False - envelope = command["envelope"] - try: - wrapper_not_before = _utc_timestamp( - envelope.get("not_before"), - "command envelope not_before", - ) - wrapper_expires = _utc_timestamp( - envelope.get("expires_at"), - "command envelope expires_at", - ) - except DumbMoneyAuthorityError as error: - raise DumbMoneyCommandFeedError(str(error)) from error - now = now_utc.astimezone(timezone.utc) - transport_current = wrapper_not_before <= now < wrapper_expires - if isinstance(verified, VerifiedCapitalAuthority): - body_not_before = _utc_timestamp( - verified.envelope.not_before, - "capital body not_before", - ) - body_expires = _utc_timestamp( - verified.envelope.expires_at, - "capital body expires_at", - ) - body_current = body_not_before <= now < body_expires - positive_grant = True - elif isinstance(verified, VerifiedDesiredMode): - body_not_before = _utc_timestamp( - verified.desired_mode.not_before, - "desired mode body not_before", - ) - body_expires = _utc_timestamp( - verified.desired_mode.expires_at, - "desired mode body expires_at", - ) - body_current = body_not_before <= now < body_expires - positive_grant = verified.desired_mode.mode == "LIVE" - else: - changed_at = _utc_timestamp( - verified.kill_state.changed_at, - "kill state changed_at", - ) - body_current = changed_at <= now + MAXIMUM_KILL_CHANGED_AT_FUTURE_SKEW - positive_grant = not verified.kill_state.active - valid_now = transport_current and body_current - expected_effect = ( - "APPLY_FAIL_CLOSED" - if not positive_grant - else "APPLY_POSITIVE" - if valid_now - else "HISTORICAL_ONLY" - ) - if command.get("transport_window_current") is not transport_current: - raise DumbMoneyCommandFeedError( - "command transport_window_current projection mismatch" - ) - if command.get("valid_now") is not valid_now: - raise DumbMoneyCommandFeedError("command valid_now projection mismatch") - if command.get("authority_effect") != expected_effect: - raise DumbMoneyCommandFeedError("command authority_effect projection mismatch") - - -def _validated_loopback_origin(base_url: str) -> str: - try: - parsed = urlsplit(base_url) - port = parsed.port - except ValueError as error: - raise DumbMoneyCommandFeedError( - "DumbMoney Core URL has an invalid port" - ) from error - if ( - parsed.scheme != "http" - or parsed.hostname != "127.0.0.1" - or port is None - or not 1 <= port <= 65_535 - or parsed.username is not None - or parsed.password is not None - or parsed.path not in {"", "/"} - or parsed.query - or parsed.fragment - ): - raise DumbMoneyCommandFeedError( - "DumbMoney Core command feed must use an exact " - "http://127.0.0.1: origin" - ) - return f"http://127.0.0.1:{port}" - - -def _cell_bearer(provider: Callable[[], str]) -> str: - try: - value = provider() - except Exception as error: - raise DumbMoneyCommandFeedError( - f"cell bearer provider failed: {type(error).__name__}" - ) from error - if ( - not isinstance(value, str) - or not 32 <= len(value) <= 4096 - or any(character.isspace() or ord(character) < 33 for character in value) - ): - raise DumbMoneyCommandFeedError( - "cell bearer must be an injected high-entropy non-whitespace string" - ) - return value - - -def _state_from_payload(payload: dict[str, Any]) -> _CommandState: - _exact_fields(payload, COMMAND_STATE_FIELDS, "command state") - if payload.get("schema") != COMMAND_STATE_SCHEMA: - raise DumbMoneyCommandFeedError("command state schema is unsupported") - if payload.get("cell_id") != DOPEY_VENUE: - raise DumbMoneyCommandFeedError("command state targets another cell") - next_sequence = _nonnegative_int( - payload.get("next_sequence"), - "command state next_sequence", - ) - next_digest = _digest( - payload.get("next_digest"), - "command state next_digest", - ) - if next_sequence == 0 and next_digest != GENESIS_DIGEST: - raise DumbMoneyCommandFeedError( - "bootstrap command cursor must use the zero digest" - ) - for field in ( - "kill_envelope", - "desired_mode_envelope", - "capital_envelope", - ): - value = payload.get(field) - if value is not None and ( - not isinstance(value, dict) - or not all(isinstance(key, str) for key in value) - ): - raise DumbMoneyCommandFeedError( - f"command state {field} must be an object or null" - ) - updated_at = str(payload.get("updated_at", "")) - try: - _utc_timestamp(updated_at, "command state updated_at") - except DumbMoneyAuthorityError as error: - raise DumbMoneyCommandFeedError(str(error)) from error - return _CommandState( - next_sequence=next_sequence, - next_digest=next_digest, - kill_envelope=payload.get("kill_envelope"), - desired_mode_envelope=payload.get("desired_mode_envelope"), - capital_envelope=payload.get("capital_envelope"), - updated_at=updated_at, - ) - - -def _read_state(path: Path) -> _CommandState: - if not path.exists(): - return _CommandState() - if not path.is_file(): - raise DumbMoneyCommandFeedError("command state path is not a file") - try: - raw = path.read_bytes() - except OSError as error: - raise DumbMoneyCommandFeedError("command state is unreadable") from error - if len(raw) > MAXIMUM_RESPONSE_BYTES: - raise DumbMoneyCommandFeedError("command state exceeds its size ceiling") - return _state_from_payload(_loads_strict_json(raw, "command state")) - - -def _write_state(path: Path, state: _CommandState) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - encoded = ( - json.dumps( - state.to_dict(), - sort_keys=True, - separators=(",", ":"), - ensure_ascii=False, - allow_nan=False, - ) - + "\n" - ).encode("utf-8") - temporary_path: Path | None = None - try: - with tempfile.NamedTemporaryFile( - mode="wb", - dir=path.parent, - prefix=f".{path.name}.", - suffix=".tmp", - delete=False, - ) as handle: - handle.write(encoded) - handle.flush() - os.fsync(handle.fileno()) - temporary_path = Path(handle.name) - os.replace(temporary_path, path) - temporary_path = None - finally: - if temporary_path is not None: - try: - temporary_path.unlink() - except FileNotFoundError: - pass - - -@contextmanager -def _exclusive_state_lock(path: Path) -> Iterator[None]: - lock_path = path.with_suffix(f"{path.suffix}.lock") - lock_path.parent.mkdir(parents=True, exist_ok=True) - try: - descriptor = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY) - except FileExistsError as error: - raise DumbMoneyCommandFeedError( - "command feed state is locked by another poller" - ) from error - try: - os.write(descriptor, str(os.getpid()).encode("ascii")) - os.fsync(descriptor) - yield - finally: - os.close(descriptor) - try: - lock_path.unlink() - except FileNotFoundError: - pass - - -def _response_content_type(headers: Mapping[str, str]) -> str: - return next( - ( - str(value).lower() - for name, value in headers.items() - if str(name).lower() == "content-type" - ), - "", - ) - - -def _current_persisted_control( - envelope: dict[str, Any] | None, - core_public_key: bytes, - operator_public_keys: Mapping[str, bytes], - expected_account_hash: str, - now_utc: datetime, -) -> VerifiedCapitalAuthority | VerifiedDesiredMode | VerifiedKillState | None: - if envelope is None: - return None - body_schema = str(envelope.get("body_schema", "")) - public_key = _command_public_key( - envelope, - body_schema, - core_public_key, - operator_public_keys, - ) - return verify_signed_control_envelope( - envelope, - public_key, - now_utc, - expected_account_hash, - enforce_current_window=True, - ) - - -def _historical_persisted_control( - envelope: dict[str, Any] | None, - core_public_key: bytes, - operator_public_keys: Mapping[str, bytes], - expected_account_hash: str, - now_utc: datetime, -) -> VerifiedCapitalAuthority | VerifiedDesiredMode | VerifiedKillState | None: - if envelope is None: - return None - body_schema = str(envelope.get("body_schema", "")) - public_key = _command_public_key( - envelope, - body_schema, - core_public_key, - operator_public_keys, - ) - return verify_signed_control_envelope( - envelope, - public_key, - now_utc, - expected_account_hash, - enforce_current_window=False, - ) - - -def _fail_closed( - state: _CommandState, - reason: str, - *, - kill_active: bool = True, - required_action: str = "FAIL_CLOSED", - caught_up: bool = False, - applied_event_ids: tuple[str, ...] = (), -) -> DumbMoneyCommandDispatch: - return DumbMoneyCommandDispatch( - desired_mode="PAUSED", - kill_active=kill_active, - allow_new_risk=False, - capital_authority=None, - fail_closed_reason=reason, - required_action=required_action, - caught_up=caught_up, - next_sequence=state.next_sequence, - next_digest=state.next_digest, - applied_event_ids=applied_event_ids, - ) - - -def _validated_page( - payload: dict[str, Any], - state: _CommandState, - now_utc: datetime, - limit: int, - core_public_key: bytes, - expected_request_nonce: str, -) -> tuple[ - int, - str, - bool, - str, - tuple[dict[str, Any], ...], -]: - _exact_fields(payload, COMMAND_PAGE_FIELDS, "command page") - if payload.get("schema") != COMMAND_PAGE_SCHEMA: - raise DumbMoneyCommandFeedError("command page schema is unsupported") - raw_checkpoint = payload.get("checkpoint") - raw_checkpoint_signature = payload.get("checkpoint_signature") - if not isinstance(raw_checkpoint, dict) or not isinstance( - raw_checkpoint_signature, - dict, - ): - raise DumbMoneyCommandFeedError( - "command page checkpoint and signature must be objects" - ) - _verify_checkpoint_signature( - raw_checkpoint, - raw_checkpoint_signature, - core_public_key, - ) - checkpoint_bindings = { - "cell_id": payload.get("cell_id"), - "request_nonce": payload.get("request_nonce"), - "observed_at": payload.get("observed_at"), - "after_sequence": payload.get("after_sequence"), - "after_digest": payload.get("after_digest"), - "next_sequence": payload.get("next_sequence"), - "next_digest": payload.get("next_digest"), - "ledger_head_sequence": payload.get("ledger_head_sequence"), - "ledger_head_digest": payload.get("ledger_head_digest"), - "required_action": payload.get("required_action"), - } - if any( - raw_checkpoint.get(field) != value - for field, value in checkpoint_bindings.items() - ): - raise DumbMoneyCommandFeedError( - "signed checkpoint does not bind command page metadata" - ) - if payload.get("cell_id") != DOPEY_VENUE: - raise DumbMoneyCommandFeedError("command page targets another cell") - request_nonce = _digest( - payload.get("request_nonce"), - "command page request_nonce", - ) - if request_nonce != expected_request_nonce: - raise DumbMoneyCommandFeedError( - "command page request_nonce does not match this poll" - ) - if payload.get("after_sequence") != state.next_sequence: - raise DumbMoneyCommandFeedError("command page sequence gap or rollback") - if payload.get("after_digest") != state.next_digest: - raise DumbMoneyCommandFeedError("command page starting digest mismatch") - next_sequence = _nonnegative_int( - payload.get("next_sequence"), - "command page next_sequence", - ) - if ( - next_sequence < state.next_sequence - or next_sequence - state.next_sequence > limit - ): - raise DumbMoneyCommandFeedError("command page sequence advance is invalid") - next_digest = _digest( - payload.get("next_digest"), - "command page next_digest", - ) - ledger_head = _nonnegative_int( - payload.get("ledger_head_sequence"), - "command page ledger_head_sequence", - ) - ledger_head_digest = _digest( - payload.get("ledger_head_digest"), - "command page ledger_head_digest", - ) - if next_sequence > ledger_head: - raise DumbMoneyCommandFeedError( - "command page cursor is ahead of the ledger head" - ) - if next_sequence == ledger_head and next_digest != ledger_head_digest: - raise DumbMoneyCommandFeedError( - "caught-up page cursor does not bind the ledger head digest" - ) - has_more = payload.get("has_more") - if not isinstance(has_more, bool) or has_more is not (next_sequence < ledger_head): - raise DumbMoneyCommandFeedError("command page has_more is inconsistent") - if next_sequence == state.next_sequence and next_digest != state.next_digest: - raise DumbMoneyCommandFeedError("stationary command cursor changed its digest") - required_action = str(payload.get("required_action", "")) - if required_action not in VALID_REQUIRED_ACTIONS: - raise DumbMoneyCommandFeedError("command page required_action is unsupported") - try: - observed_at = _utc_timestamp( - payload.get("observed_at"), - "command page observed_at", - ) - except DumbMoneyAuthorityError as error: - raise DumbMoneyCommandFeedError(str(error)) from error - now = now_utc.astimezone(timezone.utc) - if abs(now - observed_at) > MAXIMUM_PAGE_CLOCK_SKEW: - raise DumbMoneyCommandFeedError( - "command page observed_at is stale or future-dated" - ) - raw_commands = payload.get("commands") - if not isinstance(raw_commands, list) or len(raw_commands) > limit: - raise DumbMoneyCommandFeedError("command page commands must be a bounded array") - ordered_commands = raw_checkpoint.get("ordered_commands") - if not isinstance(ordered_commands, list) or len(ordered_commands) != len( - raw_commands - ): - raise DumbMoneyCommandFeedError( - "signed checkpoint ordered_commands does not match the page" - ) - commands: list[dict[str, Any]] = [] - previous_sequence = state.next_sequence - previous_event_digest = state.next_digest - event_ids: set[str] = set() - event_digests: set[str] = set() - for index, raw_command in enumerate(raw_commands): - if not isinstance(raw_command, dict): - raise DumbMoneyCommandFeedError( - f"command page command {index} must be an object" - ) - _exact_fields(raw_command, COMMAND_FIELDS, f"command {index}") - global_sequence = _positive_int( - raw_command.get("global_sequence"), - f"command {index} global_sequence", - ) - prior_sequence = previous_sequence - if not previous_sequence < global_sequence <= next_sequence: - raise DumbMoneyCommandFeedError( - "command global sequences are duplicated, reordered, " - "or outside the page" - ) - event_id = _digest(raw_command.get("event_id"), f"command {index} event_id") - event_digest = _digest( - raw_command.get("event_digest"), - f"command {index} event_digest", - ) - if event_id in event_ids or event_digest in event_digests: - raise DumbMoneyCommandFeedError( - "command page repeats an event identity or digest" - ) - event_ids.add(event_id) - event_digests.add(event_digest) - if not isinstance(raw_command.get("valid_now"), bool): - raise DumbMoneyCommandFeedError( - "command valid_now projection must be a boolean" - ) - if not isinstance( - raw_command.get("transport_window_current"), - bool, - ): - raise DumbMoneyCommandFeedError( - "command transport_window_current projection must be a boolean" - ) - if raw_command.get("authority_effect") not in VALID_AUTHORITY_EFFECTS: - raise DumbMoneyCommandFeedError( - "command authority_effect projection is unsupported" - ) - envelope = raw_command.get("envelope") - if not isinstance(envelope, dict): - raise DumbMoneyCommandFeedError( - f"command {index} envelope must be an object" - ) - if envelope.get("event_id") != event_id: - raise DumbMoneyCommandFeedError( - f"command {index} event_id does not bind its envelope" - ) - if envelope.get("body_schema") != raw_command.get("body_schema"): - raise DumbMoneyCommandFeedError( - f"command {index} body_schema does not bind its envelope" - ) - proof = _validated_ledger_proof( - raw_command.get("ledger_proof"), - raw_command, - envelope, - ) - if ( - global_sequence == state.next_sequence + 1 - and proof["previous_global_digest"] != state.next_digest - ): - raise DumbMoneyCommandFeedError( - "first contiguous ledger proof does not bind the request cursor" - ) - if ( - commands - and global_sequence == prior_sequence + 1 - and proof["previous_global_digest"] != previous_event_digest - ): - raise DumbMoneyCommandFeedError( - "contiguous ledger proofs do not form a global hash chain" - ) - ordered_command = ordered_commands[index] - if not isinstance(ordered_command, dict): - raise DumbMoneyCommandFeedError( - "signed checkpoint ordered command must be an object" - ) - _exact_fields( - ordered_command, - ORDERED_COMMAND_FIELDS, - f"checkpoint ordered command {index}", - ) - projected = { - key: value for key, value in raw_command.items() if key != "envelope" - } - if ordered_command != projected: - raise DumbMoneyCommandFeedError( - "signed checkpoint ordered command projection mismatch" - ) - commands.append(dict(raw_command)) - previous_sequence = global_sequence - previous_event_digest = event_digest - if commands and commands[-1]["global_sequence"] == next_sequence: - if commands[-1]["event_digest"] != next_digest: - raise DumbMoneyCommandFeedError( - "command page terminal event digest does not bind the next cursor" - ) - return next_sequence, next_digest, has_more, required_action, tuple(commands) - - -def _apply_commands( - state: _CommandState, - commands: tuple[dict[str, Any], ...], - core_public_key: bytes, - operator_public_keys: Mapping[str, bytes], - expected_account_hash: str, - now_utc: datetime, -) -> tuple[ - dict[str, Any] | None, - dict[str, Any] | None, - dict[str, Any] | None, - tuple[str, ...], -]: - historical_kill = _historical_persisted_control( - state.kill_envelope, - core_public_key, - operator_public_keys, - expected_account_hash, - now_utc, - ) - historical_mode = _historical_persisted_control( - state.desired_mode_envelope, - core_public_key, - operator_public_keys, - expected_account_hash, - now_utc, - ) - historical_capital = _historical_persisted_control( - state.capital_envelope, - core_public_key, - operator_public_keys, - expected_account_hash, - now_utc, - ) - if historical_kill is not None and not isinstance( - historical_kill, - VerifiedKillState, - ): - raise DumbMoneyCommandFeedError("persisted kill control has the wrong schema") - if historical_mode is not None and not isinstance( - historical_mode, - VerifiedDesiredMode, - ): - raise DumbMoneyCommandFeedError("persisted mode control has the wrong schema") - if historical_capital is not None and not isinstance( - historical_capital, - VerifiedCapitalAuthority, - ): - raise DumbMoneyCommandFeedError( - "persisted capital control has the wrong schema" - ) - kill_envelope = state.kill_envelope - desired_envelope = state.desired_mode_envelope - capital_envelope = state.capital_envelope - kill_generation = ( - historical_kill.kill_state.generation if historical_kill is not None else 0 - ) - mode_revision = ( - historical_mode.desired_mode.revision if historical_mode is not None else 0 - ) - capital_generation = ( - historical_capital.envelope.fencing_generation - if historical_capital is not None - else 0 - ) - applied_event_ids: list[str] = [] - for command in commands: - envelope = dict(command["envelope"]) - body_schema = str(command["body_schema"]) - try: - public_key = _command_public_key( - envelope, - body_schema, - core_public_key, - operator_public_keys, - ) - verified = verify_signed_control_envelope( - envelope, - public_key, - now_utc, - expected_account_hash, - enforce_current_window=False, - ) - except DumbMoneyAuthorityError as error: - raise DumbMoneyCommandFeedError( - f"signed command verification failed: {error}" - ) from error - _validate_command_projection(command, verified, now_utc) - if isinstance(verified, VerifiedKillState): - if body_schema != KILL_STATE_SCHEMA: - raise DumbMoneyCommandFeedError("verified kill schema mismatch") - if verified.kill_state.generation != kill_generation + 1: - raise DumbMoneyCommandFeedError("KillStateV1 generation gap or replay") - kill_generation = verified.kill_state.generation - kill_envelope = envelope - elif isinstance(verified, VerifiedDesiredMode): - if body_schema != DESIRED_MODE_SCHEMA: - raise DumbMoneyCommandFeedError("verified desired-mode schema mismatch") - if verified.desired_mode.revision != mode_revision + 1: - raise DumbMoneyCommandFeedError("DesiredModeV1 revision gap or replay") - mode_revision = verified.desired_mode.revision - desired_envelope = envelope - elif isinstance(verified, VerifiedCapitalAuthority): - if body_schema != CAPITAL_ENVELOPE_SCHEMA: - raise DumbMoneyCommandFeedError("verified capital schema mismatch") - if verified.envelope.fencing_generation != capital_generation + 1: - raise DumbMoneyCommandFeedError( - "CapitalEnvelopeV1 fencing generation gap or replay" - ) - capital_generation = verified.envelope.fencing_generation - capital_envelope = envelope - else: # pragma: no cover - closed by the verifier union - raise DumbMoneyCommandFeedError("unsupported verified command") - applied_event_ids.append(verified.event_id) - return ( - kill_envelope, - desired_envelope, - capital_envelope, - tuple(applied_event_ids), - ) - - -def _dispatch_from_state( - state: _CommandState, - core_public_key: bytes, - operator_public_keys: Mapping[str, bytes], - expected_account_hash: str, - now_utc: datetime, - *, - required_action: str, - caught_up: bool, - applied_event_ids: tuple[str, ...], -) -> DumbMoneyCommandDispatch: - try: - kill = _current_persisted_control( - state.kill_envelope, - core_public_key, - operator_public_keys, - expected_account_hash, - now_utc, - ) - except DumbMoneyAuthorityError: - _historical_persisted_control( - state.kill_envelope, - core_public_key, - operator_public_keys, - expected_account_hash, - now_utc, - ) - return _fail_closed( - state, - "kill_control_stale_or_future", - # A previously authenticated active kill is durable. An expired - # inactive projection is no longer positive evidence that kill is - # clear, so unknown also resolves to the risk-reducing state. - kill_active=True, - required_action=required_action, - caught_up=caught_up, - applied_event_ids=applied_event_ids, - ) - if kill is None or not isinstance(kill, VerifiedKillState): - return _fail_closed( - state, - "verified_kill_state_missing", - required_action=required_action, - caught_up=caught_up, - applied_event_ids=applied_event_ids, - ) - if kill.kill_state.active: - return _fail_closed( - state, - "signed_global_kill_active", - kill_active=True, - required_action=required_action, - caught_up=caught_up, - applied_event_ids=applied_event_ids, - ) - try: - desired = _current_persisted_control( - state.desired_mode_envelope, - core_public_key, - operator_public_keys, - expected_account_hash, - now_utc, - ) - capital = _current_persisted_control( - state.capital_envelope, - core_public_key, - operator_public_keys, - expected_account_hash, - now_utc, - ) - except DumbMoneyAuthorityError: - return _fail_closed( - state, - "mode_or_capital_control_stale_future_or_invalid", - kill_active=False, - required_action=required_action, - caught_up=caught_up, - applied_event_ids=applied_event_ids, - ) - if desired is None or not isinstance(desired, VerifiedDesiredMode): - return _fail_closed( - state, - "verified_desired_mode_missing", - kill_active=False, - required_action=required_action, - caught_up=caught_up, - applied_event_ids=applied_event_ids, - ) - if desired.desired_mode.mode != "LIVE": - dispatch = _fail_closed( - state, - f"desired_mode_{desired.desired_mode.mode.lower()}", - kill_active=False, - required_action=required_action, - caught_up=caught_up, - applied_event_ids=applied_event_ids, - ) - return DumbMoneyCommandDispatch( - **{ - **dispatch.__dict__, - "desired_mode": desired.desired_mode.mode, - } - ) - if capital is None or not isinstance(capital, VerifiedCapitalAuthority): - return _fail_closed( - state, - "verified_capital_authority_missing", - kill_active=False, - required_action=required_action, - caught_up=caught_up, - applied_event_ids=applied_event_ids, - ) - policy_epochs = { - kill.kill_state.policy_epoch, - desired.desired_mode.policy_epoch, - capital.envelope.policy_epoch, - } - if len(policy_epochs) != 1: - return _fail_closed( - state, - "control_policy_epoch_mismatch", - kill_active=False, - required_action=required_action, - caught_up=caught_up, - applied_event_ids=applied_event_ids, - ) - if not caught_up: - return _fail_closed( - state, - "command_feed_not_caught_up", - kill_active=False, - required_action=required_action, - caught_up=False, - applied_event_ids=applied_event_ids, - ) - if required_action != "APPLY_SIGNED_CONTROLS": - return _fail_closed( - state, - f"core_required_action_{required_action.lower()}", - kill_active=False, - required_action=required_action, - caught_up=caught_up, - applied_event_ids=applied_event_ids, - ) - authority = DumbMoneyAuthorityContext( - signed_envelope=dict(state.capital_envelope or {}), - trusted_public_key=core_public_key, - expected_account_hash=expected_account_hash, - ) - return DumbMoneyCommandDispatch( - desired_mode="LIVE", - kill_active=False, - allow_new_risk=True, - capital_authority=authority, - fail_closed_reason="", - required_action=required_action, - caught_up=True, - next_sequence=state.next_sequence, - next_digest=state.next_digest, - applied_event_ids=applied_event_ids, - ) - - -def poll_once( - *, - base_url: str, - cell_bearer_provider: Callable[[], str], - trusted_public_key: bytes, - trusted_operator_public_keys: Mapping[str, bytes], - expected_account_hash: str, - state_path: Path, - transport: CommandFeedTransport, - now_utc: datetime, - limit: int = DEFAULT_PAGE_LIMIT, - timeout_seconds: float = 5.0, -) -> DumbMoneyCommandDispatch: - """Fetch, verify, persist, and dispatch at most one bounded command page.""" - - if now_utc.tzinfo is None: - return _fail_closed(_CommandState(), "poll_clock_is_timezone_naive") - try: - origin = _validated_loopback_origin(base_url) - core_public_key, operator_public_keys = _trusted_keyring( - trusted_public_key, - trusted_operator_public_keys, - ) - account_hash = _sha256_text( - expected_account_hash, - "expected_account_hash", - ) - if ( - isinstance(limit, bool) - or not isinstance(limit, int) - or not 1 <= limit <= MAXIMUM_PAGE_LIMIT - ): - raise DumbMoneyCommandFeedError( - "command page limit must be between 1 and 1000" - ) - if ( - isinstance(timeout_seconds, bool) - or not isinstance(timeout_seconds, (int, float)) - or not math.isfinite(float(timeout_seconds)) - or not 0.1 <= float(timeout_seconds) <= 30.0 - ): - raise DumbMoneyCommandFeedError( - "command transport timeout must be between 0.1 and 30 seconds" - ) - resolved_state_path = state_path.resolve() - except (DumbMoneyAuthorityError, DumbMoneyCommandFeedError, OSError) as error: - return _fail_closed( - _CommandState(), - f"feed_configuration_invalid:{type(error).__name__}", - ) - try: - with _exclusive_state_lock(resolved_state_path): - state = _read_state(resolved_state_path) - try: - token = _cell_bearer(cell_bearer_provider) - try: - raw_request_nonce = secrets.token_hex(32) - except Exception as error: - raise DumbMoneyCommandFeedError( - "cryptographic request nonce generation failed" - ) from error - request_nonce = _digest( - raw_request_nonce, - "command request_nonce", - ) - query = urlencode( - { - "after": state.next_sequence, - "cursor": state.next_digest, - "limit": limit, - "request_nonce": request_nonce, - } - ) - request = CommandFeedHttpRequest( - method="GET", - url=(f"{origin}/v1/cells/{DOPEY_VENUE}/commands?{query}"), - headers={ - "Accept": "application/json", - "Authorization": f"Bearer {token}", - }, - timeout_seconds=float(timeout_seconds), - ) - try: - response = transport(request) - except Exception as error: - return _fail_closed( - state, - f"command_transport_failure:{type(error).__name__}", - ) - if not isinstance(response, CommandFeedHttpResponse): - return _fail_closed( - state, - "command_transport_response_invalid", - ) - if isinstance(response.status, bool) or not isinstance( - response.status, int - ): - return _fail_closed(state, "command_transport_status_invalid") - if response.status in {401, 403}: - return _fail_closed(state, "cell_bearer_authentication_failed") - if response.status == 409: - return _fail_closed(state, "command_cursor_rejected") - if response.status != 200: - return _fail_closed( - state, - f"command_http_status_{response.status}", - ) - if not isinstance(response.body, bytes): - return _fail_closed(state, "command_response_body_not_bytes") - if len(response.body) > MAXIMUM_RESPONSE_BYTES: - return _fail_closed(state, "command_response_too_large") - if not _response_content_type(response.headers).startswith( - "application/json" - ): - return _fail_closed( - state, - "command_response_content_type_invalid", - ) - page = _loads_strict_json(response.body, "command page") - ( - next_sequence, - next_digest, - has_more, - required_action, - commands, - ) = _validated_page( - page, - state, - now_utc, - limit, - core_public_key, - request_nonce, - ) - ( - kill_envelope, - desired_envelope, - capital_envelope, - applied_event_ids, - ) = _apply_commands( - state, - commands, - core_public_key, - operator_public_keys, - account_hash, - now_utc, - ) - advanced = _CommandState( - next_sequence=next_sequence, - next_digest=next_digest, - kill_envelope=kill_envelope, - desired_mode_envelope=desired_envelope, - capital_envelope=capital_envelope, - updated_at=_format_utc(now_utc), - ) - _write_state(resolved_state_path, advanced) - return _dispatch_from_state( - advanced, - core_public_key, - operator_public_keys, - account_hash, - now_utc, - required_action=required_action, - caught_up=not has_more, - applied_event_ids=applied_event_ids, - ) - except ( - DumbMoneyAuthorityError, - DumbMoneyCommandFeedError, - OSError, - ) as error: - return _fail_closed( - state, - f"command_page_rejected:{error}", - kill_active=True, - ) - except (DumbMoneyCommandFeedError, OSError) as error: - return _fail_closed( - _CommandState(), - f"command_state_unavailable:{error}", - ) diff --git a/dopey_live/dumbmoney_contract_feed.py b/dopey_live/dumbmoney_contract_feed.py deleted file mode 100644 index b6d90f7..0000000 --- a/dopey_live/dumbmoney_contract_feed.py +++ /dev/null @@ -1,846 +0,0 @@ -"""Nonce-bound Core resolver for Dopey promotion and passport contracts.""" - -from __future__ import annotations - -import hashlib -import math -import secrets -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from typing import Any, Callable, Final, Mapping -from urllib.parse import urlencode - -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey - -from .dumbmoney_authority import ( - DOPEY_VENUE, - DumbMoneyAuthorityError, - VerifiedCapitalAuthority, - _decode_public_key, - _sha256_text, - _utc_timestamp, - _urlsafe_b64decode, - canonical_json_bytes, -) -from .dumbmoney_command_feed import ( - CHECKPOINT_SIGNATURE_FIELDS, - MAXIMUM_RESPONSE_BYTES, - CommandFeedHttpRequest, - CommandFeedHttpResponse, - CommandFeedTransport, - DumbMoneyCommandFeedError, - _cell_bearer, - _exact_fields, - _loads_strict_json, - _response_content_type, - _validated_ledger_proof, - _validated_loopback_origin, -) -from .dumbmoney_promotion import ( - ALPHA_PASSPORT_SCHEMA, - LIVE_PROMOTION_STAGES, - PROMOTION_CERTIFICATE_SCHEMA, - DumbMoneyPromotionError, - PromotionAuthorityResolver, - VerifiedAlphaPassport, - VerifiedPromotionBinding, - VerifiedPromotionCertificate, - bind_verified_promotion, - validated_role_keyrings, - verify_alpha_passport_envelope, - verify_promotion_certificate_envelope, -) -from .models import LiveExecutionIntent - - -CONTRACT_RESOLUTION_SCHEMA: Final[str] = "dumbmoney.cell-contract-resolution.v1" -CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA: Final[str] = ( - "dumbmoney.cell-contract-resolution-checkpoint.v1" -) -CONTRACT_RESOLUTION_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "cell_id", - "request_nonce", - "requested_body_digest", - "capital_envelope_digest", - "fencing_generation", - "observed_at", - "contract_schema", - "transport_window_current", - "body_window_current", - "eligible_live_input", - "authority_state", - "ledger_proof", - "envelope", - "checkpoint", - "checkpoint_signature", - } -) -CONTRACT_RESOLUTION_CHECKPOINT_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "cell_id", - "request_nonce", - "requested_body_digest", - "capital_envelope_digest", - "fencing_generation", - "observed_at", - "contract_schema", - "transport_window_current", - "body_window_current", - "eligible_live_input", - "authority_state", - "ledger_proof", - "envelope", - } -) -CONTRACT_SCHEMAS: Final[frozenset[str]] = frozenset( - {ALPHA_PASSPORT_SCHEMA, PROMOTION_CERTIFICATE_SCHEMA} -) -MAXIMUM_RESOLUTION_CLOCK_SKEW: Final[timedelta] = timedelta(seconds=60) -AUTHORITY_STATE_SCHEMA: Final[str] = "dumbmoney.cell-authority-state.v1" -AUTHORITY_STATE_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "evaluated_at", - "authority_valid_until", - "policy_epoch", - "mandate_id", - "mandate_event_digest", - "kill_clear", - "kill_generation", - "kill_event_digest", - "desired_mode", - "desired_mode_revision", - "desired_mode_event_digest", - "capital_envelope_digest", - "capital_event_digest", - "fencing_generation", - "strategy_hash", - "passport_digest", - "passport_event_digest", - "promotion_digest", - "promotion_event_digest", - "verdicts", - "ledger_head_sequence", - "ledger_head_digest", - } -) -AUTHORITY_VERDICT_FIELDS: Final[frozenset[str]] = frozenset( - { - "verdict_digest", - "verdict_event_digest", - "verdict_id", - "court", - "decision", - "signer_key_id", - "evaluated_at", - "expires_at", - "transport_expires_at", - } -) -VERDICT_COURTS: Final[frozenset[str]] = frozenset( - {"integrity", "statistics", "economics", "adversarial_operations"} -) - - -@dataclass(frozen=True) -class _ResolvedContract: - contract_schema: str - body_digest: str - envelope: dict[str, Any] - verified: VerifiedAlphaPassport | VerifiedPromotionCertificate - authority_state: dict[str, Any] - - -def _validated_evaluator_keyring( - values: Mapping[str, bytes], - *, - disjoint_key_ids: frozenset[str], -) -> dict[str, bytes]: - if not isinstance(values, Mapping) or not 1 <= len(values) <= 32: - raise DumbMoneyPromotionError( - "sealed evaluator keyring must contain 1 to 32 keys" - ) - resolved: dict[str, bytes] = {} - for raw_key_id, raw_key in values.items(): - key_id = _sha256_text(raw_key_id, "evaluator signer key ID") - try: - public_key = _decode_public_key(raw_key) - except DumbMoneyAuthorityError as error: - raise DumbMoneyPromotionError( - "sealed evaluator public key is invalid" - ) from error - if hashlib.sha256(public_key).hexdigest() != key_id: - raise DumbMoneyPromotionError( - "evaluator signer key ID does not bind its public key" - ) - if key_id in disjoint_key_ids: - raise DumbMoneyPromotionError( - "evaluator signer role overlaps a control or research role" - ) - resolved[key_id] = public_key - return resolved - - -@dataclass(frozen=True) -class DumbMoneyContractResolverClient(PromotionAuthorityResolver): - """Fetch and authenticate the exact singleton lineage named by capital.""" - - base_url: str - cell_bearer_provider: Callable[[], str] - trusted_core_public_key: bytes - trusted_promoter_public_keys: Mapping[str, bytes] - trusted_research_public_keys: Mapping[str, bytes] - trusted_evaluator_public_keys: Mapping[str, bytes] - transport: CommandFeedTransport - timeout_seconds: float = 5.0 - - def bind_intent( - self, - authority: VerifiedCapitalAuthority, - intent: LiveExecutionIntent, - now_utc: datetime, - *, - expected_binding: VerifiedPromotionBinding | None = None, - ) -> tuple[LiveExecutionIntent, VerifiedPromotionBinding]: - if now_utc.tzinfo is None: - raise DumbMoneyPromotionError( - "contract resolution requires an aware UTC clock" - ) - if ( - len(authority.envelope.passport_hashes) != 1 - or len(authority.envelope.promotion_hashes) != 1 - ): - raise DumbMoneyPromotionError( - "contract resolver requires singleton capital lineage" - ) - try: - core_key = _decode_public_key(self.trusted_core_public_key) - except DumbMoneyAuthorityError as error: - raise DumbMoneyPromotionError( - "pinned Core public key is invalid" - ) from error - promoter_keys, research_keys = validated_role_keyrings( - self.trusted_promoter_public_keys, - self.trusted_research_public_keys, - core_public_key=core_key, - ) - evaluator_keys = _validated_evaluator_keyring( - self.trusted_evaluator_public_keys, - disjoint_key_ids=frozenset( - { - hashlib.sha256(core_key).hexdigest(), - *promoter_keys, - *research_keys, - } - ), - ) - promotion_resolution = self._resolve( - authority.envelope.promotion_hashes[0], - PROMOTION_CERTIFICATE_SCHEMA, - authority.body_digest, - authority.envelope.fencing_generation, - now_utc, - core_key, - promoter_keys, - research_keys, - evaluator_keys, - authority, - ) - if not isinstance( - promotion_resolution.verified, - VerifiedPromotionCertificate, - ): - raise DumbMoneyPromotionError( - "Core promotion resolution returned the wrong contract type" - ) - passport_digest = promotion_resolution.verified.certificate.passport_digest - if passport_digest != authority.envelope.passport_hashes[0]: - raise DumbMoneyPromotionError( - "resolved promotion points outside the singleton capital passport" - ) - passport_resolution = self._resolve( - passport_digest, - ALPHA_PASSPORT_SCHEMA, - authority.body_digest, - authority.envelope.fencing_generation, - now_utc, - core_key, - promoter_keys, - research_keys, - evaluator_keys, - authority, - ) - if not isinstance( - passport_resolution.verified, - VerifiedAlphaPassport, - ): - raise DumbMoneyPromotionError( - "Core passport resolution returned the wrong contract type" - ) - promotion_state = { - key: value - for key, value in promotion_resolution.authority_state.items() - if key != "evaluated_at" - } - passport_state = { - key: value - for key, value in passport_resolution.authority_state.items() - if key != "evaluated_at" - } - if promotion_state != passport_state: - raise DumbMoneyPromotionError( - "promotion and passport were not resolved from one authority state" - ) - return bind_verified_promotion( - authority, - intent, - passport_resolution.verified, - promotion_resolution.verified, - now_utc, - expected_binding=expected_binding, - ) - - def _resolve( - self, - body_digest: str, - expected_schema: str, - capital_envelope_digest: str, - fencing_generation: int, - now_utc: datetime, - core_public_key: bytes, - promoter_public_keys: Mapping[str, bytes], - research_public_keys: Mapping[str, bytes], - evaluator_public_keys: Mapping[str, bytes], - authority: VerifiedCapitalAuthority, - ) -> _ResolvedContract: - try: - origin = _validated_loopback_origin(self.base_url) - digest = _sha256_text(body_digest, "requested contract digest") - capital_digest = _sha256_text( - capital_envelope_digest, - "active capital envelope digest", - ) - if ( - isinstance(fencing_generation, bool) - or not isinstance(fencing_generation, int) - or fencing_generation <= 0 - ): - raise DumbMoneyPromotionError( - "active capital fencing generation must be positive" - ) - if expected_schema not in CONTRACT_SCHEMAS: - raise DumbMoneyPromotionError( - "requested contract schema is unsupported" - ) - if ( - isinstance(self.timeout_seconds, bool) - or not isinstance(self.timeout_seconds, (int, float)) - or not math.isfinite(float(self.timeout_seconds)) - or not 0.1 <= float(self.timeout_seconds) <= 30.0 - ): - raise DumbMoneyPromotionError( - "contract transport timeout must be between 0.1 and 30 seconds" - ) - token = _cell_bearer(self.cell_bearer_provider) - try: - request_nonce = _sha256_text( - secrets.token_hex(32), - "contract request_nonce", - ) - except Exception as error: - raise DumbMoneyPromotionError( - "cryptographic contract nonce generation failed" - ) from error - except (DumbMoneyAuthorityError, DumbMoneyCommandFeedError) as error: - raise DumbMoneyPromotionError( - f"contract resolver configuration is invalid: {error}" - ) from error - query = urlencode( - { - "request_nonce": request_nonce, - "capital_envelope_digest": capital_digest, - } - ) - request = CommandFeedHttpRequest( - method="GET", - url=(f"{origin}/v1/cells/{DOPEY_VENUE}/contracts/{digest}?{query}"), - headers={ - "Accept": "application/json", - "Authorization": f"Bearer {token}", - }, - timeout_seconds=float(self.timeout_seconds), - ) - try: - response = self.transport(request) - except Exception as error: - raise DumbMoneyPromotionError( - f"contract transport failed closed: {type(error).__name__}" - ) from error - if not isinstance(response, CommandFeedHttpResponse): - raise DumbMoneyPromotionError( - "contract transport returned an unsupported response" - ) - if isinstance(response.status, bool) or not isinstance(response.status, int): - raise DumbMoneyPromotionError("contract HTTP status is invalid") - if response.status in {401, 403}: - raise DumbMoneyPromotionError( - "contract resolution authentication or scope was denied" - ) - if response.status != 200: - raise DumbMoneyPromotionError( - f"contract resolver returned HTTP {response.status}" - ) - if not isinstance(response.body, bytes): - raise DumbMoneyPromotionError("contract response body is not bytes") - if len(response.body) > MAXIMUM_RESPONSE_BYTES: - raise DumbMoneyPromotionError("contract response exceeds size limit") - if not _response_content_type(response.headers).startswith("application/json"): - raise DumbMoneyPromotionError("contract response content type is invalid") - try: - payload = _loads_strict_json(response.body, "contract resolution") - return _validated_resolution( - payload, - digest, - expected_schema, - capital_digest, - fencing_generation, - request_nonce, - now_utc, - core_public_key, - promoter_public_keys, - research_public_keys, - evaluator_public_keys, - authority, - ) - except ( - DumbMoneyAuthorityError, - DumbMoneyCommandFeedError, - DumbMoneyPromotionError, - ) as error: - raise DumbMoneyPromotionError( - f"contract resolution was rejected: {error}" - ) from error - - -def _verify_resolution_checkpoint( - checkpoint: Mapping[str, Any], - signature_payload: Mapping[str, Any], - core_public_key: bytes, -) -> None: - _exact_fields( - checkpoint, - CONTRACT_RESOLUTION_CHECKPOINT_FIELDS, - "contract resolution checkpoint", - ) - _exact_fields( - signature_payload, - CHECKPOINT_SIGNATURE_FIELDS, - "contract resolution checkpoint signature", - ) - if checkpoint.get("schema") != CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA: - raise DumbMoneyPromotionError( - "contract resolution checkpoint schema is unsupported" - ) - if signature_payload.get("algorithm") != "Ed25519": - raise DumbMoneyPromotionError( - "contract resolution checkpoint algorithm is unsupported" - ) - expected_key_id = hashlib.sha256(core_public_key).hexdigest() - if signature_payload.get("signer_key_id") != expected_key_id: - raise DumbMoneyPromotionError( - "contract resolution checkpoint signer is not pinned Core" - ) - try: - signature = _urlsafe_b64decode(str(signature_payload.get("signature", ""))) - if len(signature) != 64: - raise ValueError("Ed25519 signature must be 64 bytes") - Ed25519PublicKey.from_public_bytes(core_public_key).verify( - signature, - canonical_json_bytes(dict(checkpoint)), - ) - except (InvalidSignature, ValueError) as error: - raise DumbMoneyPromotionError( - "contract resolution checkpoint signature is invalid" - ) from error - - -def _positive_sequence(value: Any, label: str) -> int: - if isinstance(value, bool) or not isinstance(value, int) or value <= 0: - raise DumbMoneyPromotionError(f"{label} must be a positive integer") - return value - - -def _validated_authority_state( - raw_state: Any, - *, - authority: VerifiedCapitalAuthority, - now_utc: datetime, - evaluator_public_keys: Mapping[str, bytes], -) -> dict[str, Any]: - if not isinstance(raw_state, dict): - raise DumbMoneyPromotionError("contract authority_state must be an object") - _exact_fields(raw_state, AUTHORITY_STATE_FIELDS, "contract authority_state") - if raw_state.get("schema") != AUTHORITY_STATE_SCHEMA: - raise DumbMoneyPromotionError("contract authority_state schema is unsupported") - now = now_utc.astimezone(timezone.utc) - evaluated_at = _utc_timestamp( - raw_state.get("evaluated_at"), - "authority_state.evaluated_at", - ) - valid_until = _utc_timestamp( - raw_state.get("authority_valid_until"), - "authority_state.authority_valid_until", - ) - if ( - abs(now - evaluated_at) > MAXIMUM_RESOLUTION_CLOCK_SKEW - or not evaluated_at < valid_until - or not now < valid_until - or valid_until - > _utc_timestamp( - authority.envelope.expires_at, - "active capital envelope expires_at", - ) - ): - raise DumbMoneyPromotionError( - "contract authority_state is stale, future-dated, or expired" - ) - expected_values = { - "policy_epoch": authority.envelope.policy_epoch, - "mandate_id": authority.envelope.mandate_id, - "kill_clear": True, - "desired_mode": "LIVE", - "capital_envelope_digest": authority.body_digest, - "fencing_generation": authority.envelope.fencing_generation, - "strategy_hash": authority.envelope.strategy_hashes[0], - "passport_digest": authority.envelope.passport_hashes[0], - "promotion_digest": authority.envelope.promotion_hashes[0], - } - for field, expected in expected_values.items(): - if raw_state.get(field) != expected: - raise DumbMoneyPromotionError( - f"contract authority_state {field} does not match active authority" - ) - for field in ( - "kill_generation", - "desired_mode_revision", - "ledger_head_sequence", - ): - _positive_sequence(raw_state.get(field), f"authority_state.{field}") - for field in ( - "mandate_event_digest", - "kill_event_digest", - "desired_mode_event_digest", - "capital_event_digest", - "passport_event_digest", - "promotion_event_digest", - "ledger_head_digest", - ): - _sha256_text(raw_state.get(field), f"authority_state.{field}") - verdicts = raw_state.get("verdicts") - if not isinstance(verdicts, list) or len(verdicts) < 2: - raise DumbMoneyPromotionError( - "contract authority_state requires at least two PASS verdicts" - ) - verdict_facts: set[tuple[str, str, str, str]] = set() - courts: set[str] = set() - for index, verdict in enumerate(verdicts): - if not isinstance(verdict, dict): - raise DumbMoneyPromotionError( - f"authority_state.verdicts[{index}] must be an object" - ) - _exact_fields( - verdict, - AUTHORITY_VERDICT_FIELDS, - f"authority_state.verdicts[{index}]", - ) - digest = _sha256_text( - verdict.get("verdict_digest"), - f"authority_state.verdicts[{index}].verdict_digest", - ) - event_digest = _sha256_text( - verdict.get("verdict_event_digest"), - f"authority_state.verdicts[{index}].verdict_event_digest", - ) - verdict_id = str(verdict.get("verdict_id", "")) - if not verdict_id or len(verdict_id) > 128: - raise DumbMoneyPromotionError("authority verdict ID is invalid") - court = str(verdict.get("court", "")) - if court not in VERDICT_COURTS or verdict.get("decision") != "PASS": - raise DumbMoneyPromotionError( - "contract authority verdict is not a recognized PASS" - ) - signer_key_id = _sha256_text( - verdict.get("signer_key_id"), - f"authority_state.verdicts[{index}].signer_key_id", - ) - if signer_key_id not in evaluator_public_keys: - raise DumbMoneyPromotionError( - "contract authority verdict signer is not sealed" - ) - verdict_evaluated_at = _utc_timestamp( - verdict.get("evaluated_at"), - f"authority_state.verdicts[{index}].evaluated_at", - ) - verdict_expires_at = _utc_timestamp( - verdict.get("expires_at"), - f"authority_state.verdicts[{index}].expires_at", - ) - transport_expires_at = _utc_timestamp( - verdict.get("transport_expires_at"), - f"authority_state.verdicts[{index}].transport_expires_at", - ) - if ( - verdict_evaluated_at > now + MAXIMUM_RESOLUTION_CLOCK_SKEW - or not verdict_evaluated_at < verdict_expires_at - or not now < verdict_expires_at - or not now < transport_expires_at - or valid_until > min(verdict_expires_at, transport_expires_at) - ): - raise DumbMoneyPromotionError( - "contract authority verdict is stale, future-dated, or expired" - ) - verdict_facts.add((digest, event_digest, verdict_id, court)) - courts.add(court) - if len(verdict_facts) != len(verdicts) or len(courts) < 2: - raise DumbMoneyPromotionError( - "contract authority verdict facts or courts are not unique" - ) - return dict(raw_state) - - -def _validated_resolution( - payload: dict[str, Any], - expected_digest: str, - expected_schema: str, - expected_capital_digest: str, - expected_fencing_generation: int, - expected_nonce: str, - now_utc: datetime, - core_public_key: bytes, - promoter_public_keys: Mapping[str, bytes], - research_public_keys: Mapping[str, bytes], - evaluator_public_keys: Mapping[str, bytes], - authority: VerifiedCapitalAuthority, -) -> _ResolvedContract: - _exact_fields(payload, CONTRACT_RESOLUTION_FIELDS, "contract resolution") - if payload.get("schema") != CONTRACT_RESOLUTION_SCHEMA: - raise DumbMoneyPromotionError("contract resolution schema is unsupported") - checkpoint = payload.get("checkpoint") - checkpoint_signature = payload.get("checkpoint_signature") - if not isinstance(checkpoint, dict) or not isinstance( - checkpoint_signature, - dict, - ): - raise DumbMoneyPromotionError( - "contract resolution checkpoint and signature must be objects" - ) - _verify_resolution_checkpoint( - checkpoint, - checkpoint_signature, - core_public_key, - ) - expected_checkpoint = { - key: value - for key, value in payload.items() - if key not in {"schema", "checkpoint", "checkpoint_signature"} - } - expected_checkpoint["schema"] = CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA - if checkpoint != expected_checkpoint: - raise DumbMoneyPromotionError( - "signed checkpoint does not bind the full contract resolution" - ) - if payload.get("cell_id") != DOPEY_VENUE: - raise DumbMoneyPromotionError("contract resolution targets another venue cell") - request_nonce = _sha256_text( - payload.get("request_nonce"), - "contract response request_nonce", - ) - if request_nonce != expected_nonce: - raise DumbMoneyPromotionError( - "contract response nonce does not match this request" - ) - requested_digest = _sha256_text( - payload.get("requested_body_digest"), - "contract response requested_body_digest", - ) - if requested_digest != expected_digest: - raise DumbMoneyPromotionError( - "contract response digest does not match this request" - ) - capital_digest = _sha256_text( - payload.get("capital_envelope_digest"), - "contract response capital_envelope_digest", - ) - if capital_digest != expected_capital_digest: - raise DumbMoneyPromotionError( - "contract response is not bound to the active capital envelope" - ) - fencing_generation = payload.get("fencing_generation") - if ( - isinstance(fencing_generation, bool) - or not isinstance(fencing_generation, int) - or fencing_generation != expected_fencing_generation - ): - raise DumbMoneyPromotionError( - "contract response is not bound to the active capital fence" - ) - contract_schema = str(payload.get("contract_schema", "")) - if contract_schema != expected_schema: - raise DumbMoneyPromotionError( - "contract response schema does not match this request" - ) - for field in ( - "transport_window_current", - "body_window_current", - "eligible_live_input", - ): - if not isinstance(payload.get(field), bool): - raise DumbMoneyPromotionError( - f"contract response {field} must be a boolean" - ) - observed_at = _utc_timestamp( - payload.get("observed_at"), - "contract response observed_at", - ) - now = now_utc.astimezone(timezone.utc) - if abs(now - observed_at) > MAXIMUM_RESOLUTION_CLOCK_SKEW: - raise DumbMoneyPromotionError( - "contract response observed_at is stale or future-dated" - ) - envelope = payload.get("envelope") - if not isinstance(envelope, dict): - raise DumbMoneyPromotionError("contract response envelope must be an object") - if envelope.get("body_digest") != expected_digest: - raise DumbMoneyPromotionError( - "contract envelope body digest does not match the requested digest" - ) - if envelope.get("body_schema") != expected_schema: - raise DumbMoneyPromotionError( - "contract envelope schema does not match the requested schema" - ) - authority_state = _validated_authority_state( - payload.get("authority_state"), - authority=authority, - now_utc=now_utc, - evaluator_public_keys=evaluator_public_keys, - ) - if ( - _utc_timestamp( - authority_state["evaluated_at"], - "authority_state.evaluated_at", - ) - != observed_at - ): - raise DumbMoneyPromotionError( - "contract authority_state evaluated_at does not mirror observed_at" - ) - raw_proof = payload.get("ledger_proof") - if not isinstance(raw_proof, dict): - raise DumbMoneyPromotionError("contract ledger proof must be an object") - proof_command = { - "global_sequence": raw_proof.get("global_sequence"), - "event_id": raw_proof.get("event_id"), - "event_digest": raw_proof.get("event_digest"), - "body_schema": expected_schema, - } - proof = _validated_ledger_proof( - raw_proof, - proof_command, - envelope, - ) - if ( - proof["payload_digest"] != expected_digest - or proof["event_schema"] != expected_schema - ): - raise DumbMoneyPromotionError( - "contract ledger proof does not bind the requested contract" - ) - expected_authority_event_digest = authority_state[ - ( - "promotion_event_digest" - if expected_schema == PROMOTION_CERTIFICATE_SCHEMA - else "passport_event_digest" - ) - ] - if proof["event_digest"] != expected_authority_event_digest: - raise DumbMoneyPromotionError( - "contract proof event digest does not match current authority_state" - ) - ledger_head_sequence = int(authority_state["ledger_head_sequence"]) - proof_sequence = int(proof["global_sequence"]) - if proof_sequence > ledger_head_sequence or ( - proof_sequence == ledger_head_sequence - and proof["event_digest"] != authority_state["ledger_head_digest"] - ): - raise DumbMoneyPromotionError( - "contract proof is not continuous with the authority ledger head" - ) - if expected_schema == PROMOTION_CERTIFICATE_SCHEMA: - promotion_verified = verify_promotion_certificate_envelope( - envelope, - promoter_public_keys, - now_utc, - ) - verified: VerifiedAlphaPassport | VerifiedPromotionCertificate = ( - promotion_verified - ) - body_not_before = _utc_timestamp( - promotion_verified.certificate.not_before, - "promotion.not_before", - ) - body_expires = _utc_timestamp( - promotion_verified.certificate.expires_at, - "promotion.expires_at", - ) - eligible = ( - body_not_before <= observed_at < body_expires - and promotion_verified.certificate.stage in LIVE_PROMOTION_STAGES - ) - else: - passport_verified = verify_alpha_passport_envelope( - envelope, - research_public_keys, - now_utc, - ) - verified = passport_verified - body_not_before = _utc_timestamp( - passport_verified.passport.created_at, - "passport.created_at", - ) - body_expires = _utc_timestamp( - passport_verified.passport.expires_at, - "passport.expires_at", - ) - eligible = body_not_before <= observed_at < body_expires - wrapper_not_before = _utc_timestamp( - envelope.get("not_before"), - "contract envelope not_before", - ) - wrapper_expires = _utc_timestamp( - envelope.get("expires_at"), - "contract envelope expires_at", - ) - transport_current = wrapper_not_before <= observed_at < wrapper_expires - body_current = body_not_before <= observed_at < body_expires - eligible = transport_current and eligible - if payload["transport_window_current"] is not transport_current: - raise DumbMoneyPromotionError("contract transport-window projection mismatch") - if payload["body_window_current"] is not body_current: - raise DumbMoneyPromotionError("contract body-window projection mismatch") - if payload["eligible_live_input"] is not eligible: - raise DumbMoneyPromotionError("contract live-eligibility projection mismatch") - if not eligible: - raise DumbMoneyPromotionError( - "resolved contract is not an eligible current live input" - ) - return _ResolvedContract( - contract_schema=contract_schema, - body_digest=expected_digest, - envelope=dict(envelope), - verified=verified, - authority_state=authority_state, - ) diff --git a/dopey_live/dumbmoney_journal_anchor.py b/dopey_live/dumbmoney_journal_anchor.py deleted file mode 100644 index b15fc86..0000000 --- a/dopey_live/dumbmoney_journal_anchor.py +++ /dev/null @@ -1,595 +0,0 @@ -"""Core-backed rollback detection for Dopey's venue-local state stores.""" - -from __future__ import annotations - -import hashlib -import secrets -from collections.abc import Callable -from dataclasses import dataclass -from datetime import UTC, datetime, timedelta -from typing import Any, Final -from urllib.parse import urlsplit - -from cryptography.exceptions import InvalidSignature -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey - -from .dumbmoney_authority import ( - DOPEY_VENUE, - IDENTIFIER_PATTERN, - DumbMoneyAuthorityError, - _decode_public_key, - _exact_fields, - _loads_strict_signed_json, - _sha256_text, - _urlsafe_b64decode, - _utc_timestamp, - _verify_signed_wrapper, - canonical_json_bytes, -) -from .dumbmoney_command_feed import ( - CommandFeedHttpRequest, - CommandFeedHttpResponse, - CommandFeedTransport, -) - -ANCHOR_REQUEST_SCHEMA: Final[str] = "dumbmoney.cell-journal-anchor-request.v1" -ANCHOR_RESPONSE_SCHEMA: Final[str] = "dumbmoney.cell-journal-anchor-response.v1" -ANCHOR_CHECKPOINT_SCHEMA: Final[str] = "dumbmoney.cell-journal-anchor-checkpoint.v1" -ANCHOR_BODY_SCHEMA: Final[str] = "dumbmoney.cell-journal-head-anchor.v1" -LEDGER_PROOF_SCHEMA: Final[str] = "dumbmoney.ledger-event-proof.v1" -CORE_SOURCE_ID: Final[str] = "dumbmoney-core" -ZERO_DIGEST: Final[str] = "0" * 64 -MAXIMUM_RESPONSE_BYTES: Final[int] = 524_288 -MAXIMUM_RESPONSE_AGE: Final[timedelta] = timedelta(seconds=30) -MAXIMUM_CLOCK_SKEW: Final[timedelta] = timedelta(seconds=5) -MAXIMUM_ANCHOR_TTL: Final[timedelta] = timedelta(seconds=120) -_RESPONSE_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "cell_id", - "request_nonce", - "observed_at", - "reused", - "anchor_envelope", - "ledger_proof", - "checkpoint", - "checkpoint_signature", - } -) -_CHECKPOINT_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "cell_id", - "request_nonce", - "account_hash", - "journal_name", - "journal_schema", - "journal_stream_id", - "journal_sequence", - "journal_head_sha256", - "anchor_body_digest", - "anchor_event_digest", - "reused", - "observed_at", - } -) -_SIGNATURE_FIELDS: Final[frozenset[str]] = frozenset( - {"algorithm", "signer_key_id", "signature"} -) -_ANCHOR_BODY_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "anchor_id", - "venue", - "account_hash", - "journal_name", - "journal_schema", - "journal_stream_id", - "journal_sequence", - "journal_head_sha256", - "previous_anchor_body_digest", - "anchored_at", - } -) -_LEDGER_PROOF_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "global_sequence", - "event_id", - "source_id", - "source_sequence", - "signer_key_id", - "nonce", - "event_schema", - "observed_at", - "received_at", - "correlation_id", - "causation_id", - "payload_digest", - "previous_source_digest", - "previous_global_digest", - "event_digest", - } -) - - -class JournalAnchorError(RuntimeError): - """A Core anchor, signature, or monotonicity check failed closed.""" - - -@dataclass(frozen=True) -class VerifiedJournalAnchor: - journal_name: str - journal_sequence: int - journal_head_sha256: str - anchor_body_digest: str - anchor_event_digest: str - reused: bool - observed_at: str - - -def _hash(payload: dict[str, Any]) -> str: - return hashlib.sha256(canonical_json_bytes(payload)).hexdigest() - - -def _nonnegative_int(value: Any, label: str) -> int: - if isinstance(value, bool) or not isinstance(value, int) or value < 0: - raise JournalAnchorError(f"{label} must be a nonnegative integer") - return value - - -def _positive_int(value: Any, label: str) -> int: - result = _nonnegative_int(value, label) - if result == 0: - raise JournalAnchorError(f"{label} must be positive") - return result - - -def _digest(value: Any, label: str) -> str: - try: - return _sha256_text(value, label) - except DumbMoneyAuthorityError as error: - raise JournalAnchorError(str(error)) from error - - -def _timestamp(value: Any, label: str) -> datetime: - try: - return _utc_timestamp(value, label) - except DumbMoneyAuthorityError as error: - raise JournalAnchorError(str(error)) from error - - -def _strict_response(body: bytes) -> dict[str, Any]: - if not isinstance(body, bytes) or not 1 <= len(body) <= MAXIMUM_RESPONSE_BYTES: - raise JournalAnchorError("Core anchor response size is invalid") - try: - return _loads_strict_signed_json( - body, - "Core anchor response", - require_canonical_bytes=False, - ) - except DumbMoneyAuthorityError as error: - raise JournalAnchorError(str(error)) from error - - -class CoreJournalAnchorClient: - """Publish exact local heads and verify Core's durable signed receipt.""" - - def __init__( - self, - *, - base_url: str, - transport: CommandFeedTransport, - cell_token_provider: Callable[[], str], - trusted_core_public_key: bytes, - expected_account_hash: str, - request_nonce_fn: Callable[[], str] = lambda: secrets.token_hex(32), - now_fn: Callable[[], datetime] = lambda: datetime.now(UTC), - ) -> None: - parsed = urlsplit(base_url) - try: - port = parsed.port - except ValueError as error: - raise JournalAnchorError("Core anchor base URL port is invalid") from error - if ( - parsed.scheme != "http" - or parsed.hostname not in {"127.0.0.1", "::1"} - or port is None - or not 1024 <= port <= 65_535 - or parsed.username is not None - or parsed.password is not None - or parsed.path not in {"", "/"} - or parsed.query - or parsed.fragment - ): - raise JournalAnchorError( - "Core anchor base URL must be literal loopback HTTP" - ) - try: - self._core_public_key = _decode_public_key(trusted_core_public_key) - except DumbMoneyAuthorityError as error: - raise JournalAnchorError("pinned Core public key is invalid") from error - self._core_key_id = hashlib.sha256(self._core_public_key).hexdigest() - self._base_url = base_url.rstrip("/") - self._transport = transport - self._cell_token_provider = cell_token_provider - self._account_hash = _digest( - expected_account_hash, - "expected_account_hash", - ) - self._request_nonce_fn = request_nonce_fn - self._now_fn = now_fn - self._last_nonce: str | None = None - - def stream_id(self, journal_name: str) -> str: - if not isinstance(journal_name, str) or not IDENTIFIER_PATTERN.fullmatch( - journal_name - ): - raise JournalAnchorError("journal_name must be a canonical identifier") - return _hash( - { - "schema": "dopey.journal-anchor-stream.v1", - "cell_id": DOPEY_VENUE, - "account_hash": self._account_hash, - "journal_name": journal_name, - } - ) - - def _verify_checkpoint( - self, - checkpoint: Any, - signature: Any, - ) -> dict[str, Any]: - if not isinstance(checkpoint, dict): - raise JournalAnchorError("Core anchor checkpoint must be an object") - try: - _exact_fields( - checkpoint, - _CHECKPOINT_FIELDS, - "Core anchor checkpoint", - ) - except DumbMoneyAuthorityError as error: - raise JournalAnchorError(str(error)) from error - if not isinstance(signature, dict): - raise JournalAnchorError( - "Core anchor checkpoint signature must be an object" - ) - try: - _exact_fields( - signature, - _SIGNATURE_FIELDS, - "Core anchor checkpoint signature", - ) - except DumbMoneyAuthorityError as error: - raise JournalAnchorError(str(error)) from error - if ( - signature.get("algorithm") != "Ed25519" - or signature.get("signer_key_id") != self._core_key_id - ): - raise JournalAnchorError( - "Core anchor checkpoint signature identity mismatch" - ) - try: - raw_signature = _urlsafe_b64decode(str(signature.get("signature", ""))) - if len(raw_signature) != 64: - raise ValueError("Ed25519 signature must be 64 bytes") - Ed25519PublicKey.from_public_bytes(self._core_public_key).verify( - raw_signature, - canonical_json_bytes(checkpoint), - ) - except (InvalidSignature, ValueError) as error: - raise JournalAnchorError( - "Core anchor checkpoint signature is invalid" - ) from error - return dict(checkpoint) - - @staticmethod - def _verify_ledger_proof( - proof: Any, - *, - signed: Any, - observed_at: datetime, - ) -> dict[str, Any]: - if not isinstance(proof, dict): - raise JournalAnchorError("anchor ledger proof must be an object") - try: - _exact_fields(proof, _LEDGER_PROOF_FIELDS, "anchor ledger proof") - except DumbMoneyAuthorityError as error: - raise JournalAnchorError(str(error)) from error - if proof.get("schema") != LEDGER_PROOF_SCHEMA: - raise JournalAnchorError("anchor ledger proof schema is unsupported") - global_sequence = _positive_int( - proof.get("global_sequence"), - "ledger proof global_sequence", - ) - source_sequence = _positive_int( - proof.get("source_sequence"), - "ledger proof source_sequence", - ) - received_at = _timestamp( - proof.get("received_at"), - "ledger proof received_at", - ) - record_observed = _timestamp( - proof.get("observed_at"), - "ledger proof observed_at", - ) - if received_at > observed_at or record_observed != max( - received_at, - signed.not_before_utc, - ): - raise JournalAnchorError("anchor ledger proof observation semantics differ") - previous_global = _digest( - proof.get("previous_global_digest"), - "ledger proof previous_global_digest", - ) - previous_source = _digest( - proof.get("previous_source_digest"), - "ledger proof previous_source_digest", - ) - if (global_sequence == 1) != (previous_global == ZERO_DIGEST): - raise JournalAnchorError( - "anchor ledger proof global predecessor is invalid" - ) - if (source_sequence == 1) != (previous_source == ZERO_DIGEST): - raise JournalAnchorError( - "anchor ledger proof source predecessor is invalid" - ) - expected = { - "event_id": signed.event_id, - "source_id": signed.source_id, - "source_sequence": signed.source_sequence, - "signer_key_id": signed.signer_key_id, - "event_schema": signed.body_schema, - "payload_digest": signed.body_digest, - } - for field, expected_value in expected.items(): - if proof.get(field) != expected_value: - raise JournalAnchorError( - f"anchor ledger proof {field} differs from envelope" - ) - event_digest = _digest( - proof.get("event_digest"), - "ledger proof event_digest", - ) - material = { - key: value - for key, value in proof.items() - if key not in {"schema", "event_digest"} - } - if _hash(material) != event_digest: - raise JournalAnchorError("anchor ledger proof digest is invalid") - return dict(proof) - - def anchor( - self, - *, - journal_name: str, - journal_schema: str, - journal_sequence: int, - journal_head_sha256: str, - ) -> VerifiedJournalAnchor: - if ( - not isinstance(journal_name, str) - or not IDENTIFIER_PATTERN.fullmatch(journal_name) - or not isinstance(journal_schema, str) - or not IDENTIFIER_PATTERN.fullmatch(journal_schema) - ): - raise JournalAnchorError( - "journal name and schema must be canonical identifiers" - ) - sequence = _nonnegative_int(journal_sequence, "journal_sequence") - head = _digest(journal_head_sha256, "journal_head_sha256") - if (sequence == 0) != (head == ZERO_DIGEST): - raise JournalAnchorError( - "journal sequence zero must exactly bind the zero head" - ) - stream_id = self.stream_id(journal_name) - nonce = _digest(self._request_nonce_fn(), "request_nonce") - if nonce == self._last_nonce: - raise JournalAnchorError("journal anchor nonce generator repeated a nonce") - self._last_nonce = nonce - request = { - "schema": ANCHOR_REQUEST_SCHEMA, - "cell_id": DOPEY_VENUE, - "account_hash": self._account_hash, - "journal_name": journal_name, - "journal_schema": journal_schema, - "journal_stream_id": stream_id, - "journal_sequence": sequence, - "journal_head_sha256": head, - "request_nonce": nonce, - } - token = self._cell_token_provider() - if ( - not isinstance(token, str) - or not token - or token != token.strip() - or any(character.isspace() for character in token) - ): - raise JournalAnchorError("cell token is not a canonical secret") - try: - response = self._transport( - CommandFeedHttpRequest( - method="POST", - url=( - f"{self._base_url}/v1/cells/{DOPEY_VENUE}/journal-heads:anchor" - ), - headers={ - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - "Accept": "application/json", - }, - timeout_seconds=5.0, - body=canonical_json_bytes(request), - ) - ) - except Exception as error: - raise JournalAnchorError( - f"journal anchor transport failed:{type(error).__name__}" - ) from error - finally: - token = "" - if not isinstance(response, CommandFeedHttpResponse) or response.status != 200: - raise JournalAnchorError("Core rejected the journal head anchor") - content_type = next( - ( - str(value).split(";", 1)[0].strip().lower() - for name, value in response.headers.items() - if str(name).lower() == "content-type" - ), - "", - ) - if content_type != "application/json": - raise JournalAnchorError("Core anchor response content type is invalid") - payload = _strict_response(response.body) - try: - _exact_fields(payload, _RESPONSE_FIELDS, "Core anchor response") - except DumbMoneyAuthorityError as error: - raise JournalAnchorError(str(error)) from error - if ( - payload.get("schema") != ANCHOR_RESPONSE_SCHEMA - or payload.get("cell_id") != DOPEY_VENUE - or payload.get("request_nonce") != nonce - or not isinstance(payload.get("reused"), bool) - ): - raise JournalAnchorError("Core anchor response fields or identity mismatch") - checkpoint = self._verify_checkpoint( - payload.get("checkpoint"), - payload.get("checkpoint_signature"), - ) - observed_at = _timestamp( - payload.get("observed_at"), - "anchor response observed_at", - ) - now = self._now_fn() - if now.tzinfo is None or now.utcoffset() is None: - raise JournalAnchorError("journal anchor clock must be timezone-aware") - now = now.astimezone(UTC) - if ( - observed_at > now + MAXIMUM_CLOCK_SKEW - or now - observed_at > MAXIMUM_RESPONSE_AGE - ): - raise JournalAnchorError("Core anchor response is not current") - expected_checkpoint = { - "schema": ANCHOR_CHECKPOINT_SCHEMA, - "cell_id": DOPEY_VENUE, - "request_nonce": nonce, - "account_hash": self._account_hash, - "journal_name": journal_name, - "journal_schema": journal_schema, - "journal_stream_id": stream_id, - "journal_sequence": sequence, - "journal_head_sha256": head, - "anchor_body_digest": checkpoint.get("anchor_body_digest"), - "anchor_event_digest": checkpoint.get("anchor_event_digest"), - "reused": payload["reused"], - "observed_at": payload["observed_at"], - } - if checkpoint != expected_checkpoint: - raise JournalAnchorError("Core anchor checkpoint differs from the request") - envelope = payload.get("anchor_envelope") - if not isinstance(envelope, dict): - raise JournalAnchorError("Core journal anchor envelope must be an object") - try: - signed = _verify_signed_wrapper( - envelope, - self._core_public_key, - observed_at, - enforce_current_window=False, - ) - except DumbMoneyAuthorityError as error: - raise JournalAnchorError( - "Core journal anchor envelope is invalid" - ) from error - if ( - signed.source_id != CORE_SOURCE_ID - or signed.body_schema != ANCHOR_BODY_SCHEMA - or signed.expires_at_utc - signed.not_before_utc > MAXIMUM_ANCHOR_TTL - ): - raise JournalAnchorError( - "Core journal anchor envelope authority is invalid" - ) - body = signed.body - try: - _exact_fields(body, _ANCHOR_BODY_FIELDS, "journal anchor body") - except DumbMoneyAuthorityError as error: - raise JournalAnchorError(str(error)) from error - anchored_at = _timestamp( - body.get("anchored_at"), - "anchor body anchored_at", - ) - if ( - body.get("venue") != DOPEY_VENUE - or body.get("account_hash") != self._account_hash - or body.get("journal_name") != journal_name - or body.get("journal_schema") != journal_schema - or body.get("journal_stream_id") != stream_id - or body.get("journal_sequence") != sequence - or body.get("journal_head_sha256") != head - or anchored_at > observed_at - ): - raise JournalAnchorError("journal anchor body differs from the local head") - previous = _digest( - body.get("previous_anchor_body_digest"), - "previous_anchor_body_digest", - ) - identity = { - "schema": ANCHOR_BODY_SCHEMA, - "venue": DOPEY_VENUE, - "account_hash": self._account_hash, - "journal_name": journal_name, - "journal_schema": journal_schema, - "journal_stream_id": stream_id, - "journal_sequence": sequence, - "journal_head_sha256": head, - "previous_anchor_body_digest": previous, - } - if body.get("anchor_id") != _hash(identity): - raise JournalAnchorError("journal anchor identity is invalid") - proof = self._verify_ledger_proof( - payload.get("ledger_proof"), - signed=signed, - observed_at=observed_at, - ) - for field in ("nonce", "correlation_id", "causation_id"): - if proof.get(field) != envelope.get(field): - raise JournalAnchorError( - f"anchor ledger proof {field} differs from envelope" - ) - body_digest = _digest( - checkpoint.get("anchor_body_digest"), - "checkpoint anchor_body_digest", - ) - event_digest = _digest( - checkpoint.get("anchor_event_digest"), - "checkpoint anchor_event_digest", - ) - if ( - body_digest != signed.body_digest - or body_digest != proof.get("payload_digest") - or event_digest != proof.get("event_digest") - ): - raise JournalAnchorError( - "journal anchor checkpoint proof binding is invalid" - ) - return VerifiedJournalAnchor( - journal_name=journal_name, - journal_sequence=sequence, - journal_head_sha256=head, - anchor_body_digest=body_digest, - anchor_event_digest=event_digest, - reused=bool(payload["reused"]), - observed_at=str(payload["observed_at"]), - ) - - -__all__ = [ - "ANCHOR_BODY_SCHEMA", - "ANCHOR_CHECKPOINT_SCHEMA", - "ANCHOR_REQUEST_SCHEMA", - "ANCHOR_RESPONSE_SCHEMA", - "CoreJournalAnchorClient", - "JournalAnchorError", - "VerifiedJournalAnchor", -] diff --git a/dopey_live/dumbmoney_promotion.py b/dopey_live/dumbmoney_promotion.py deleted file mode 100644 index 4d61a0c..0000000 --- a/dopey_live/dumbmoney_promotion.py +++ /dev/null @@ -1,585 +0,0 @@ -"""Authenticated DumbMoney promotion lineage for Robinhood entry intents. - -An entry strategy hash is not authority. The only accepted lineage is an -AlphaPassportV1 signed by a sealed research key and a PromotionCertificateV1 -signed by a disjoint sealed promoter key. Their canonical body digests must -also be the one singleton tuple named by the active CapitalEnvelopeV1. -""" - -from __future__ import annotations - -import hashlib -from dataclasses import dataclass, replace -from datetime import datetime, timezone -from typing import Any, Final, Mapping, Protocol - -from .dumbmoney_authority import ( - DOPEY_VENUE, - EQUITY_INSTRUMENT_ID_PATTERN, - IDENTIFIER_PATTERN, - OPTION_INSTRUMENT_ID_PATTERN, - DumbMoneyAuthorityError, - VerifiedCapitalAuthority, - _decode_public_key, - _exact_fields, - _hash_array, - _identifier_array, - _positive_int, - _sha256_text, - _utc_timestamp, - _verify_signed_wrapper, - intent_authorized_instrument_id, - intent_incremental_risk_cents, -) -from .models import LiveExecutionIntent - - -ALPHA_PASSPORT_SCHEMA: Final[str] = "dumbmoney.alpha_passport.v1" -PROMOTION_CERTIFICATE_SCHEMA: Final[str] = "dumbmoney.promotion-certificate.v1" -LIVE_PROMOTION_STAGES: Final[frozenset[str]] = frozenset( - {"EXPLORATORY_LIVE", "AGGRESSIVE_BOUNDED"} -) -EVIDENCE_CLASSES: Final[frozenset[str]] = frozenset( - {"SYNTHETIC", "REPLAY", "BACKTEST", "PAPER", "FORWARD", "REALIZED"} -) -PROMOTION_STAGES: Final[frozenset[str]] = frozenset( - { - "IDEA", - "REPLAY", - "POINT_IN_TIME_BACKTEST", - "FORWARD_SHADOW", - "PAPER", - *LIVE_PROMOTION_STAGES, - } -) -ALPHA_PASSPORT_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "passport_id", - "strategy_lineage_id", - "venue", - "strategy_hash", - "artifact_hashes", - "evidence_verdict_hashes", - "intended_instruments", - "maximum_loss_cents", - "evidence_class", - "created_at", - "expires_at", - } -) -PROMOTION_CERTIFICATE_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "certificate_id", - "passport_digest", - "verdict_digests", - "stage", - "venue", - "instruments", - "maximum_loss_cents", - "rollback_triggers", - "policy_epoch", - "not_before", - "expires_at", - } -) - - -class DumbMoneyPromotionError(DumbMoneyAuthorityError): - """Raised when signed research-to-live lineage cannot be proven.""" - - -@dataclass(frozen=True) -class AlphaPassportV1: - passport_id: str - strategy_lineage_id: str - venue: str - strategy_hash: str - artifact_hashes: tuple[str, ...] - evidence_verdict_hashes: tuple[str, ...] - intended_instruments: tuple[str, ...] - maximum_loss_cents: int - evidence_class: str - created_at: str - expires_at: str - - -@dataclass(frozen=True) -class PromotionCertificateV1: - certificate_id: str - passport_digest: str - verdict_digests: tuple[str, ...] - stage: str - venue: str - instruments: tuple[str, ...] - maximum_loss_cents: int - rollback_triggers: tuple[str, ...] - policy_epoch: int - not_before: str - expires_at: str - - -@dataclass(frozen=True) -class VerifiedAlphaPassport: - passport: AlphaPassportV1 - event_id: str - body_digest: str - signer_key_id: str - wrapper_expires_at: str - - -@dataclass(frozen=True) -class VerifiedPromotionCertificate: - certificate: PromotionCertificateV1 - event_id: str - body_digest: str - signer_key_id: str - wrapper_expires_at: str - - -@dataclass(frozen=True) -class VerifiedPromotionBinding: - """Exact signed lineage pinned to one active capital fence.""" - - strategy_hash: str - passport_hash: str - promotion_hash: str - instrument_id: str - maximum_loss_cents: int - policy_epoch: int - capital_envelope_id: str - capital_event_id: str - capital_body_digest: str - capital_fencing_generation: int - passport_event_id: str - promotion_event_id: str - passport_signer_key_id: str - promotion_signer_key_id: str - expires_at: str - - def evidence(self) -> dict[str, Any]: - return { - "strategy_hash": self.strategy_hash, - "passport_hash": self.passport_hash, - "promotion_hash": self.promotion_hash, - "instrument_id": self.instrument_id, - "maximum_loss_cents": self.maximum_loss_cents, - "policy_epoch": self.policy_epoch, - "capital_envelope_id": self.capital_envelope_id, - "capital_event_id": self.capital_event_id, - "capital_body_digest": self.capital_body_digest, - "capital_fencing_generation": self.capital_fencing_generation, - "passport_event_id": self.passport_event_id, - "promotion_event_id": self.promotion_event_id, - "passport_signer_key_id": self.passport_signer_key_id, - "promotion_signer_key_id": self.promotion_signer_key_id, - "expires_at": self.expires_at, - } - - -class PromotionAuthorityResolver(Protocol): - """Resolve and authenticate the lineage immediately before use.""" - - def bind_intent( - self, - authority: VerifiedCapitalAuthority, - intent: LiveExecutionIntent, - now_utc: datetime, - *, - expected_binding: VerifiedPromotionBinding | None = None, - ) -> tuple[LiveExecutionIntent, VerifiedPromotionBinding]: ... - - -def _identifier(value: Any, label: str) -> str: - text = str(value) - if not IDENTIFIER_PATTERN.fullmatch(text): - raise DumbMoneyPromotionError(f"{label} is not a canonical identifier") - return text - - -def _instrument_array(payload: dict[str, Any], field: str) -> tuple[str, ...]: - values = _identifier_array(payload, field) - unsupported = tuple( - value - for value in values - if not ( - EQUITY_INSTRUMENT_ID_PATTERN.fullmatch(value) - or OPTION_INSTRUMENT_ID_PATTERN.fullmatch(value) - ) - ) - if unsupported: - raise DumbMoneyPromotionError( - f"{field} contains noncanonical Dopey instrument IDs: {unsupported}" - ) - return values - - -def _possibly_empty_hash_array( - payload: dict[str, Any], - field: str, -) -> tuple[str, ...]: - values = payload.get(field) - if not isinstance(values, list): - raise DumbMoneyPromotionError(f"{field} must be an array") - normalized = tuple(_sha256_text(value, field) for value in values) - if normalized != tuple(sorted(set(normalized))): - raise DumbMoneyPromotionError(f"{field} must be sorted and duplicate-free") - return normalized - - -def _sealed_role_keyring( - keys: Mapping[str, bytes], - role: str, -) -> dict[str, bytes]: - if not isinstance(keys, Mapping) or not 1 <= len(keys) <= 16: - raise DumbMoneyPromotionError( - f"sealed {role} public-key set must contain 1 to 16 keys" - ) - verified: dict[str, bytes] = {} - for raw_key_id, raw_key in keys.items(): - key_id = _sha256_text(raw_key_id, f"{role} key ID") - try: - public_key = _decode_public_key(raw_key) - except DumbMoneyAuthorityError as error: - raise DumbMoneyPromotionError( - f"sealed {role} public key is invalid: key_id={key_id}" - ) from error - if hashlib.sha256(public_key).hexdigest() != key_id: - raise DumbMoneyPromotionError( - f"sealed {role} key ID does not bind its key: key_id={key_id}" - ) - verified[key_id] = public_key - return verified - - -def validated_role_keyrings( - promoter_public_keys: Mapping[str, bytes], - research_public_keys: Mapping[str, bytes], - *, - core_public_key: bytes | None = None, -) -> tuple[dict[str, bytes], dict[str, bytes]]: - promoter = _sealed_role_keyring(promoter_public_keys, "promoter") - research = _sealed_role_keyring(research_public_keys, "research") - overlap = set(promoter) & set(research) - if overlap: - raise DumbMoneyPromotionError( - "sealed promoter and research public-key sets must be disjoint" - ) - if core_public_key is not None: - try: - core_key_id = hashlib.sha256( - _decode_public_key(core_public_key) - ).hexdigest() - except DumbMoneyAuthorityError as error: - raise DumbMoneyPromotionError( - "pinned Core public key is invalid" - ) from error - if core_key_id in promoter or core_key_id in research: - raise DumbMoneyPromotionError( - "Core, promoter, and research signing roles must be disjoint" - ) - return promoter, research - - -def _alpha_passport( - payload: dict[str, Any], - now_utc: datetime, -) -> AlphaPassportV1: - _exact_fields(payload, ALPHA_PASSPORT_FIELDS, "AlphaPassportV1") - if payload.get("schema") != ALPHA_PASSPORT_SCHEMA: - raise DumbMoneyPromotionError("AlphaPassportV1 schema is unsupported") - if payload.get("venue") != DOPEY_VENUE: - raise DumbMoneyPromotionError("AlphaPassportV1 is not for dopey_robinhood") - evidence_class = str(payload.get("evidence_class", "")) - if evidence_class not in EVIDENCE_CLASSES: - raise DumbMoneyPromotionError("AlphaPassportV1 evidence_class is unsupported") - created_at = str(payload.get("created_at", "")) - expires_at = str(payload.get("expires_at", "")) - created = _utc_timestamp(created_at, "passport.created_at") - expires = _utc_timestamp(expires_at, "passport.expires_at") - if expires <= created: - raise DumbMoneyPromotionError("AlphaPassportV1 validity interval is empty") - now = now_utc.astimezone(timezone.utc) - if not created <= now < expires: - raise DumbMoneyPromotionError("AlphaPassportV1 is not currently valid") - return AlphaPassportV1( - passport_id=_identifier(payload.get("passport_id"), "passport_id"), - strategy_lineage_id=_identifier( - payload.get("strategy_lineage_id"), - "strategy_lineage_id", - ), - venue=DOPEY_VENUE, - strategy_hash=_sha256_text( - payload.get("strategy_hash"), - "passport.strategy_hash", - ), - artifact_hashes=_hash_array(payload, "artifact_hashes"), - evidence_verdict_hashes=_possibly_empty_hash_array( - payload, - "evidence_verdict_hashes", - ), - intended_instruments=_instrument_array(payload, "intended_instruments"), - maximum_loss_cents=_positive_int(payload, "maximum_loss_cents"), - evidence_class=evidence_class, - created_at=created_at, - expires_at=expires_at, - ) - - -def _promotion_certificate( - payload: dict[str, Any], - now_utc: datetime, -) -> PromotionCertificateV1: - _exact_fields( - payload, - PROMOTION_CERTIFICATE_FIELDS, - "PromotionCertificateV1", - ) - if payload.get("schema") != PROMOTION_CERTIFICATE_SCHEMA: - raise DumbMoneyPromotionError("PromotionCertificateV1 schema is unsupported") - if payload.get("venue") != DOPEY_VENUE: - raise DumbMoneyPromotionError( - "PromotionCertificateV1 is not for dopey_robinhood" - ) - stage = str(payload.get("stage", "")) - if stage not in PROMOTION_STAGES: - raise DumbMoneyPromotionError("PromotionCertificateV1 stage is unsupported") - not_before = str(payload.get("not_before", "")) - expires_at = str(payload.get("expires_at", "")) - starts = _utc_timestamp(not_before, "promotion.not_before") - expires = _utc_timestamp(expires_at, "promotion.expires_at") - if expires <= starts: - raise DumbMoneyPromotionError( - "PromotionCertificateV1 validity interval is empty" - ) - now = now_utc.astimezone(timezone.utc) - if not starts <= now < expires: - raise DumbMoneyPromotionError("PromotionCertificateV1 is not currently valid") - rollback_triggers = _identifier_array(payload, "rollback_triggers") - return PromotionCertificateV1( - certificate_id=_identifier( - payload.get("certificate_id"), - "certificate_id", - ), - passport_digest=_sha256_text( - payload.get("passport_digest"), - "promotion.passport_digest", - ), - verdict_digests=_hash_array(payload, "verdict_digests"), - stage=stage, - venue=DOPEY_VENUE, - instruments=_instrument_array(payload, "instruments"), - maximum_loss_cents=_positive_int(payload, "maximum_loss_cents"), - rollback_triggers=rollback_triggers, - policy_epoch=_positive_int(payload, "policy_epoch"), - not_before=not_before, - expires_at=expires_at, - ) - - -def verify_alpha_passport_envelope( - envelope: dict[str, Any], - research_public_keys: Mapping[str, bytes], - now_utc: datetime, -) -> VerifiedAlphaPassport: - """Authenticate and parse one current research-signed passport.""" - - research = _sealed_role_keyring(research_public_keys, "research") - signer_key_id = _sha256_text( - envelope.get("signer_key_id"), - "passport signer_key_id", - ) - public_key = research.get(signer_key_id) - if public_key is None: - raise DumbMoneyPromotionError( - "AlphaPassportV1 signer is outside the sealed research key set" - ) - wrapper = _verify_signed_wrapper( - envelope, - public_key, - now_utc, - enforce_current_window=True, - ) - if wrapper.body_schema != ALPHA_PASSPORT_SCHEMA: - raise DumbMoneyPromotionError("resolved envelope is not AlphaPassportV1") - passport = _alpha_passport(wrapper.body, now_utc) - return VerifiedAlphaPassport( - passport=passport, - event_id=wrapper.event_id, - body_digest=wrapper.body_digest, - signer_key_id=wrapper.signer_key_id, - wrapper_expires_at=wrapper.expires_at, - ) - - -def verify_promotion_certificate_envelope( - envelope: dict[str, Any], - promoter_public_keys: Mapping[str, bytes], - now_utc: datetime, -) -> VerifiedPromotionCertificate: - """Authenticate and parse one current promoter-signed certificate.""" - - promoter = _sealed_role_keyring(promoter_public_keys, "promoter") - signer_key_id = _sha256_text( - envelope.get("signer_key_id"), - "promotion signer_key_id", - ) - public_key = promoter.get(signer_key_id) - if public_key is None: - raise DumbMoneyPromotionError( - "PromotionCertificateV1 signer is outside the sealed promoter key set" - ) - wrapper = _verify_signed_wrapper( - envelope, - public_key, - now_utc, - enforce_current_window=True, - ) - if wrapper.body_schema != PROMOTION_CERTIFICATE_SCHEMA: - raise DumbMoneyPromotionError("resolved envelope is not PromotionCertificateV1") - promotion = _promotion_certificate(wrapper.body, now_utc) - return VerifiedPromotionCertificate( - certificate=promotion, - event_id=wrapper.event_id, - body_digest=wrapper.body_digest, - signer_key_id=wrapper.signer_key_id, - wrapper_expires_at=wrapper.expires_at, - ) - - -def bind_verified_promotion( - authority: VerifiedCapitalAuthority, - intent: LiveExecutionIntent, - passport: VerifiedAlphaPassport, - promotion: VerifiedPromotionCertificate, - now_utc: datetime, - *, - expected_binding: VerifiedPromotionBinding | None = None, -) -> tuple[LiveExecutionIntent, VerifiedPromotionBinding]: - """Bind one locally generated intent to an authenticated singleton chain.""" - - if intent.side != "buy": - raise DumbMoneyPromotionError( - "promotion lineage may only be attached to new-risk buy intents" - ) - if now_utc.tzinfo is None: - raise DumbMoneyPromotionError("promotion binding requires an aware UTC clock") - if expected_binding is None: - if intent.passport_hash or intent.promotion_hash: - raise DumbMoneyPromotionError( - "local intent supplied its own passport or promotion digest" - ) - else: - if ( - intent.passport_hash != expected_binding.passport_hash - or intent.promotion_hash != expected_binding.promotion_hash - ): - raise DumbMoneyPromotionError( - "revalidated intent lineage differs from its authenticated binding" - ) - - if ( - len(authority.envelope.strategy_hashes) != 1 - or len(authority.envelope.passport_hashes) != 1 - or len(authority.envelope.promotion_hashes) != 1 - ): - raise DumbMoneyPromotionError( - "active capital authority is not a singleton lineage tuple" - ) - strategy_hash = authority.envelope.strategy_hashes[0] - passport_hash = authority.envelope.passport_hashes[0] - promotion_hash = authority.envelope.promotion_hashes[0] - if passport.body_digest != passport_hash: - raise DumbMoneyPromotionError( - "resolved passport digest differs from active capital authority" - ) - if promotion.body_digest != promotion_hash: - raise DumbMoneyPromotionError( - "resolved promotion digest differs from active capital authority" - ) - if promotion.certificate.passport_digest != passport.body_digest: - raise DumbMoneyPromotionError( - "PromotionCertificateV1 does not bind the resolved AlphaPassportV1" - ) - if passport.passport.strategy_hash != strategy_hash: - raise DumbMoneyPromotionError( - "AlphaPassportV1 strategy differs from active capital authority" - ) - if intent.source_hash != strategy_hash: - raise DumbMoneyPromotionError( - "local intent strategy differs from authenticated singleton lineage" - ) - if promotion.certificate.stage not in LIVE_PROMOTION_STAGES: - raise DumbMoneyPromotionError( - "PromotionCertificateV1 stage is not live-authorizing" - ) - if promotion.certificate.policy_epoch != authority.envelope.policy_epoch: - raise DumbMoneyPromotionError("promotion and capital policy epochs differ") - - instrument_id = intent_authorized_instrument_id(intent) - if ( - instrument_id not in authority.envelope.authorized_instruments - or instrument_id not in passport.passport.intended_instruments - or instrument_id not in promotion.certificate.instruments - ): - raise DumbMoneyPromotionError( - "intent instrument is not jointly authorized by capital, " - "passport, and promotion" - ) - risk_cents = intent_incremental_risk_cents(intent) - lineage_maximum_loss = min( - passport.passport.maximum_loss_cents, - promotion.certificate.maximum_loss_cents, - ) - if risk_cents > lineage_maximum_loss: - raise DumbMoneyPromotionError( - "intent worst-case loss exceeds authenticated promotion lineage" - ) - expiry_candidates = ( - authority.envelope.expires_at, - passport.wrapper_expires_at, - passport.passport.expires_at, - promotion.wrapper_expires_at, - promotion.certificate.expires_at, - ) - expires_at = min( - expiry_candidates, - key=lambda value: _utc_timestamp(value, "lineage expiry"), - ) - if now_utc.astimezone(timezone.utc) >= _utc_timestamp( - expires_at, - "lineage expiry", - ): - raise DumbMoneyPromotionError( - "authenticated promotion binding is already expired" - ) - binding = VerifiedPromotionBinding( - strategy_hash=strategy_hash, - passport_hash=passport_hash, - promotion_hash=promotion_hash, - instrument_id=instrument_id, - maximum_loss_cents=lineage_maximum_loss, - policy_epoch=authority.envelope.policy_epoch, - capital_envelope_id=authority.envelope.envelope_id, - capital_event_id=authority.event_id, - capital_body_digest=authority.body_digest, - capital_fencing_generation=authority.envelope.fencing_generation, - passport_event_id=passport.event_id, - promotion_event_id=promotion.event_id, - passport_signer_key_id=passport.signer_key_id, - promotion_signer_key_id=promotion.signer_key_id, - expires_at=expires_at, - ) - if expected_binding is not None and binding != expected_binding: - raise DumbMoneyPromotionError( - "promotion lineage or capital fence changed during execution" - ) - return ( - replace( - intent, - passport_hash=passport_hash, - promotion_hash=promotion_hash, - ), - binding, - ) diff --git a/dopey_live/dumbmoney_windows_service.py b/dopey_live/dumbmoney_windows_service.py deleted file mode 100644 index 0a06e86..0000000 --- a/dopey_live/dumbmoney_windows_service.py +++ /dev/null @@ -1,1790 +0,0 @@ -"""Private-local Windows service runner for DumbMoneyDopeyRobinhood. - -The production entry point accepts only release-manifest arguments, loads one -SHA-256-pinned public config from ProgramData, reads the two secret values from -Windows Credential Manager, and starts in RECONCILIATION_ONLY. Importing this -module performs no I/O, starts no thread, and makes no network request. -""" - -from __future__ import annotations - -import argparse -import base64 -import ctypes -import hashlib -import json -import os -import re -import signal -import socket -import struct -import subprocess -import sys -import threading -import uuid -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from pathlib import Path -from typing import Any, Callable, Final, Mapping, Protocol -from urllib.parse import urlsplit - -import requests -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -from .control import ( - CONTROL_SCHEMA, - OFFICIAL_ROBINHOOD_MCP, - LiveBranchControl, -) -from .dumbmoney_authority import ( - DumbMoneyAuthorityError, - _decode_public_key, - _exact_fields, - _sha256_text, - _utc_timestamp, - _verify_signed_wrapper, - canonical_json_bytes, -) -from .dumbmoney_command_feed import ( - COMMAND_STATE_SCHEMA, - CommandFeedHttpRequest, - CommandFeedHttpResponse, - CommandFeedTransport, - DumbMoneyCommandDispatch, - DumbMoneyCommandFeedClient, - DumbMoneyCommandFeedError, - command_feed_head, -) -from .dumbmoney_contract_feed import DumbMoneyContractResolverClient -from .dumbmoney_journal_anchor import ( - CoreJournalAnchorClient, - JournalAnchorError, -) -from .ledger import ( - GENESIS_HASH, - LEDGER_SCHEMA, - LiveBranchLedgerError, - ledger_path, - paper_twin_path, - read_ledger, -) -from .robinhood_gateway import ( - LOCAL_MODEL_BASE_URL, - LOCAL_MODEL_PATTERN, - LOCAL_MODEL_PROVIDER, - RobinhoodAgenticGateway, -) -from .robinhood_mcp_direct import DeterministicRobinhoodMcpClient -from .service import run_live_branch_cycle - -SERVICE_NAME: Final[str] = "DumbMoneyDopeyRobinhood" -CONFIG_SCHEMA: Final[str] = "dopey.dumbmoney-robinhood-runner-config.v1" -CORE_READINESS_SCHEMA: Final[str] = "dumbmoney.readiness-descriptor.v1" -HEALTH_SCHEMA: Final[str] = "dumbmoney.health.v1" -SIGNED_ENVELOPE_SCHEMA: Final[str] = "dumbmoney.signed-envelope.v1" -CORE_ENDPOINT_REF: Final[str] = "endpoint-ref:DumbMoneyCore" -CELL_TOKEN_TARGET_REF: Final[str] = "credential-target:DumbMoney/DopeyCellToken" -ROBINHOOD_PROFILE_TARGET_REF: Final[str] = ( - "credential-target:DumbMoney/RobinhoodProfile" -) -START_MODE: Final[str] = "RECONCILIATION_ONLY" -READINESS_REF: Final[str] = "readiness-ref:DumbMoneyDopeyRobinhood" -CELL_TOKEN_SECRET_REF: Final[str] = "secret-ref:dumbmoney-dopey-cell-token" -ROBINHOOD_PROFILE_SECRET_REF: Final[str] = "secret-ref:robinhood-agentic-mcp-profile" -CONFIG_RELATIVE_PATH: Final[Path] = ( - Path("DumbMoney") / "config" / "dopey-robinhood-runner.v1.json" -) -CORE_READINESS_RELATIVE_PATH: Final[Path] = ( - Path("DumbMoney") / "readiness" / "DumbMoneyCore.json" -) -READINESS_RELATIVE_PATH: Final[Path] = ( - Path("DumbMoney") / "readiness" / f"{SERVICE_NAME}.json" -) -DATA_ROOT_RELATIVE_PATH: Final[Path] = Path("DumbMoney") / "dopey-robinhood" -OLLAMA_EVIDENCE_RELATIVE_PATH: Final[Path] = ( - Path("evidence") / "ollama-runtime-evidence.v1.json" -) -MAXIMUM_CONFIG_BYTES: Final[int] = 262_144 -MAXIMUM_READINESS_BYTES: Final[int] = 262_144 -MAXIMUM_OLLAMA_MODELS_BYTES: Final[int] = 1_048_576 -OLLAMA_MODELS_URL: Final[str] = f"{LOCAL_MODEL_BASE_URL.removesuffix('/v1')}/api/tags" -READINESS_KDF_DOMAIN: Final[bytes] = b"DumbMoneyDopeyRobinhood/readiness-ed25519/v1\0" -SHA256_PATTERN: Final[re.Pattern[str]] = re.compile(r"^[0-9a-f]{64}$") -WINDOWS_SID_PATTERN: Final[re.Pattern[str]] = re.compile( - r"^S-1-5-(?:[0-9]+-){1,14}[0-9]+$" -) -CONFIG_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "service_name", - "release_id", - "core_endpoint_ref", - "core_readiness_path", - "core_cell_token_target", - "robinhood_profile_target", - "start_mode", - "data_root", - "readiness_ref", - "readiness_path", - "core_public_key_base64url", - "operator_public_keys_base64url", - "promoter_public_keys_base64url", - "research_public_keys_base64url", - "evaluator_public_keys_base64url", - "expected_account_hash", - "fund_lock_sha256", - "service_manifest_sha256", - "role_public_keys_sha256", - "core_runner_config_sha256", - "risk_policy_sha256", - "readiness_signer_public_key_sha256", - "codex_executable_path", - "codex_executable_sha256", - "codex_local_provider", - "codex_local_model", - "codex_local_model_digest", - "ollama_executable_path", - "ollama_executable_sha256", - "ollama_process_identity_sid", - "ollama_runtime_evidence_sha256", - "poll_interval_seconds", - "readiness_ttl_seconds", - } -) -CORE_READINESS_FIELDS: Final[frozenset[str]] = frozenset( - { - "schema", - "service_name", - "release_id", - "instance_id", - "process_id", - "generation", - "observed_at", - "valid_until", - "endpoint", - "fund_lock_sha256", - "service_manifest_sha256", - "authority", - "health", - "capabilities", - } -) - - -class DumbMoneyWindowsServiceError(RuntimeError): - """A production service invariant failed closed.""" - - -class CredentialProvider(Protocol): - def read_bytes(self, target: str) -> bytes: ... - - -class WindowsCredentialManager: - """Read one Windows generic credential without logging its value.""" - - CRED_TYPE_GENERIC = 1 - - def read_bytes(self, target: str) -> bytes: - if not isinstance(target, str) or not re.fullmatch( - r"[A-Za-z0-9][A-Za-z0-9._/-]{0,127}", target - ): - raise DumbMoneyWindowsServiceError("Windows credential target is invalid") - if os.name != "nt": - raise DumbMoneyWindowsServiceError("Windows Credential Manager is required") - from ctypes import wintypes - - class FileTime(ctypes.Structure): - _fields_ = [ - ("low_datetime", wintypes.DWORD), - ("high_datetime", wintypes.DWORD), - ] - - class CredentialAttribute(ctypes.Structure): - _fields_ = [ - ("keyword", wintypes.LPWSTR), - ("flags", wintypes.DWORD), - ("value_size", wintypes.DWORD), - ("value", ctypes.POINTER(ctypes.c_ubyte)), - ] - - class Credential(ctypes.Structure): - _fields_ = [ - ("flags", wintypes.DWORD), - ("credential_type", wintypes.DWORD), - ("target_name", wintypes.LPWSTR), - ("comment", wintypes.LPWSTR), - ("last_written", FileTime), - ("credential_blob_size", wintypes.DWORD), - ("credential_blob", ctypes.POINTER(ctypes.c_ubyte)), - ("persist", wintypes.DWORD), - ("attribute_count", wintypes.DWORD), - ("attributes", ctypes.POINTER(CredentialAttribute)), - ("target_alias", wintypes.LPWSTR), - ("user_name", wintypes.LPWSTR), - ] - - pointer = ctypes.POINTER(Credential)() - advapi32 = ctypes.WinDLL("Advapi32.dll", use_last_error=True) - cred_read = advapi32.CredReadW - cred_read.argtypes = [ - wintypes.LPCWSTR, - wintypes.DWORD, - wintypes.DWORD, - ctypes.POINTER(ctypes.POINTER(Credential)), - ] - cred_read.restype = wintypes.BOOL - cred_free = advapi32.CredFree - cred_free.argtypes = [ctypes.c_void_p] - cred_free.restype = None - if not cred_read( - target, - self.CRED_TYPE_GENERIC, - 0, - ctypes.byref(pointer), - ): - error_code = ctypes.get_last_error() - raise DumbMoneyWindowsServiceError( - f"required Windows credential is unavailable; winerror={error_code}" - ) - try: - credential = pointer.contents - size = int(credential.credential_blob_size) - if not 1 <= size <= 16_384: - raise DumbMoneyWindowsServiceError( - "Windows credential blob size is invalid" - ) - return ctypes.string_at(credential.credential_blob, size) - finally: - cred_free(pointer) - - -@dataclass(frozen=True) -class DopeyRunnerConfig: - release_id: str - core_readiness_path: Path - data_root: Path - readiness_path: Path - core_public_key: bytes - operator_public_keys: Mapping[str, bytes] - promoter_public_keys: Mapping[str, bytes] - research_public_keys: Mapping[str, bytes] - evaluator_public_keys: Mapping[str, bytes] - expected_account_hash: str - fund_lock_sha256: str - service_manifest_sha256: str - role_public_keys_sha256: str - core_runner_config_sha256: str - risk_policy_sha256: str - readiness_signer_public_key_sha256: str - codex_executable_path: Path - codex_executable_sha256: str - codex_local_model: str - codex_local_model_digest: str - ollama_executable_path: Path - ollama_executable_sha256: str - ollama_process_identity_sid: str - ollama_runtime_evidence_sha256: str - poll_interval_seconds: int - readiness_ttl_seconds: int - config_sha256: str - - -@dataclass(frozen=True) -class CoreEndpoint: - base_url: str - instance_id: str - observed_at: str - valid_until: str - - -@dataclass -class RunnerState: - mode: str = START_MODE - health_status: str = "DEGRADED" - reason: str = "STARTING" - core_ready: bool = False - reconciled_once: bool = False - execution_enabled: bool = False - last_cycle_status: str = "not_started" - command_sequence: int = 0 - command_digest: str = "0" * 64 - rollback_anchor_current: bool = False - - -class LoopbackHttpTransport(CommandFeedTransport): - """HTTP transport that never uses proxies, redirects, or non-loopback hosts.""" - - def __init__(self) -> None: - self._session = requests.Session() - self._session.trust_env = False - - def __call__( - self, - request: CommandFeedHttpRequest, - ) -> CommandFeedHttpResponse: - parsed = urlsplit(request.url) - if ( - parsed.scheme != "http" - or parsed.hostname not in {"127.0.0.1", "::1"} - or parsed.username is not None - or parsed.password is not None - ): - raise DumbMoneyWindowsServiceError( - "Core transport attempted a non-loopback request" - ) - response = self._session.request( - request.method, - request.url, - headers=dict(request.headers), - data=request.body, - timeout=request.timeout_seconds, - allow_redirects=False, - ) - return CommandFeedHttpResponse( - status=response.status_code, - headers=dict(response.headers), - body=bytes(response.content), - ) - - -def _sha256_file(path: Path, label: str) -> str: - digest = hashlib.sha256() - try: - with path.open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - except OSError as error: - raise DumbMoneyWindowsServiceError(f"{label} cannot be read") from error - return digest.hexdigest() - - -def _windows_loopback_listener_owner( - host: str, - port: int, -) -> tuple[int, Path, str]: - """Resolve one exact IPv4 loopback listener to its PID and image path.""" - - if os.name != "nt": - raise DumbMoneyWindowsServiceError( - "Windows TCP listener ownership verification is required" - ) - if ( - host != "127.0.0.1" - or isinstance(port, bool) - or not isinstance(port, int) - or not 1 <= port <= 65_535 - ): - raise DumbMoneyWindowsServiceError("loopback listener endpoint is invalid") - from ctypes import wintypes - - class TcpRowOwnerPid(ctypes.Structure): - _fields_ = [ - ("state", wintypes.DWORD), - ("local_address", wintypes.DWORD), - ("local_port", wintypes.DWORD), - ("remote_address", wintypes.DWORD), - ("remote_port", wintypes.DWORD), - ("owning_process_id", wintypes.DWORD), - ] - - get_table = ctypes.WinDLL("Iphlpapi.dll", use_last_error=True).GetExtendedTcpTable - get_table.argtypes = [ - ctypes.c_void_p, - ctypes.POINTER(wintypes.DWORD), - wintypes.BOOL, - wintypes.ULONG, - ctypes.c_int, - wintypes.ULONG, - ] - get_table.restype = wintypes.DWORD - size = wintypes.DWORD(0) - error = get_table(None, ctypes.byref(size), False, socket.AF_INET, 3, 0) - if error not in {0, 122} or size.value <= ctypes.sizeof(wintypes.DWORD): - raise DumbMoneyWindowsServiceError( - "Windows TCP listener inventory is unavailable" - ) - buffer = ctypes.create_string_buffer(size.value) - error = get_table(buffer, ctypes.byref(size), False, socket.AF_INET, 3, 0) - if error != 0: - raise DumbMoneyWindowsServiceError( - "Windows TCP listener inventory cannot be read" - ) - count = ctypes.cast(buffer, ctypes.POINTER(wintypes.DWORD)).contents.value - rows_address = ctypes.addressof(buffer) + ctypes.sizeof(wintypes.DWORD) - rows = ctypes.cast(rows_address, ctypes.POINTER(TcpRowOwnerPid)) - owners: set[int] = set() - for index in range(count): - row = rows[index] - address = socket.inet_ntoa(struct.pack(" None: - if ( - _sha256_file( - self.static_evidence_path, - "Ollama static installation attestation", - ) - != self.static_evidence_sha256 - ): - raise DumbMoneyWindowsServiceError( - "Ollama static installation attestation SHA-256 mismatch" - ) - owner_before = self._verify_listener_owner() - response = self.transport( - CommandFeedHttpRequest( - method="GET", - url=OLLAMA_MODELS_URL, - headers={ - "Accept": "application/json", - "Cache-Control": "no-store", - }, - timeout_seconds=5.0, - ) - ) - if response.status != 200: - raise DumbMoneyWindowsServiceError( - "local Ollama model inventory is unavailable" - ) - if not 1 <= len(response.body) <= MAXIMUM_OLLAMA_MODELS_BYTES: - raise DumbMoneyWindowsServiceError( - "local Ollama model inventory size is invalid" - ) - payload = _strict_json(response.body, "local Ollama model inventory") - models = payload.get("models") - if not isinstance(models, list) or len(models) > 1_000: - raise DumbMoneyWindowsServiceError( - "local Ollama model inventory is invalid" - ) - matches = [ - item - for item in models - if isinstance(item, dict) - and item.get("name") == self.model - and item.get("model") == self.model - ] - if len(matches) != 1: - raise DumbMoneyWindowsServiceError( - "pinned local Ollama model is absent or ambiguous" - ) - if matches[0].get("digest") != self.digest: - raise DumbMoneyWindowsServiceError( - "pinned local Ollama model digest mismatch" - ) - owner_after = self._verify_listener_owner() - if owner_after != owner_before: - raise DumbMoneyWindowsServiceError( - "Ollama listener ownership changed during verification" - ) - - def _verify_listener_owner(self) -> tuple[int, Path, str]: - process_id, process_path, process_sid = self.listener_owner_resolver( - "127.0.0.1", - 11_434, - ) - if ( - isinstance(process_id, bool) - or not isinstance(process_id, int) - or process_id <= 0 - or process_path.resolve() != self.executable_path.resolve() - or process_sid != self.process_identity_sid - ): - raise DumbMoneyWindowsServiceError( - "Ollama listener is not owned by the pinned identity and executable" - ) - if _sha256_file(process_path, "Ollama listener executable") != ( - self.executable_sha256 - ): - raise DumbMoneyWindowsServiceError( - "Ollama listener executable SHA-256 mismatch" - ) - return process_id, process_path.resolve(), process_sid - - -def _strict_json(raw: bytes, label: str) -> dict[str, Any]: - def reject_duplicate(pairs: list[tuple[str, Any]]) -> dict[str, Any]: - result: dict[str, Any] = {} - for key, value in pairs: - if key in result: - raise DumbMoneyWindowsServiceError( - f"{label} contains a duplicate JSON key" - ) - result[key] = value - return result - - def reject_constant(value: str) -> None: - raise DumbMoneyWindowsServiceError( - f"{label} contains a non-finite number: {value}" - ) - - try: - payload = json.loads( - raw, - object_pairs_hook=reject_duplicate, - parse_constant=reject_constant, - ) - except (UnicodeDecodeError, json.JSONDecodeError) as error: - raise DumbMoneyWindowsServiceError( - f"{label} is not valid UTF-8 JSON" - ) from error - if not isinstance(payload, dict) or not all( - isinstance(key, str) for key in payload - ): - raise DumbMoneyWindowsServiceError(f"{label} must be a JSON object") - return payload - - -def _digest(value: Any, label: str) -> str: - try: - return _sha256_text(value, label) - except DumbMoneyAuthorityError as error: - raise DumbMoneyWindowsServiceError(str(error)) from error - - -def _windows_sid(value: Any, label: str) -> str: - if not isinstance(value, str) or not WINDOWS_SID_PATTERN.fullmatch(value): - raise DumbMoneyWindowsServiceError(f"{label} must be a canonical Windows SID") - return value - - -def _exact_path(value: Any, expected: Path, label: str) -> Path: - if not isinstance(value, str) or not value: - raise DumbMoneyWindowsServiceError(f"{label} must be an absolute path") - try: - resolved = Path(value).resolve() - except OSError as error: - raise DumbMoneyWindowsServiceError(f"{label} cannot be resolved") from error - if resolved != expected.resolve(): - raise DumbMoneyWindowsServiceError( - f"{label} differs from the fixed ProgramData path" - ) - return resolved - - -def _absolute_file_path(value: Any, label: str) -> Path: - if not isinstance(value, str) or not value: - raise DumbMoneyWindowsServiceError(f"{label} must be an absolute path") - path = Path(value) - if not path.is_absolute(): - raise DumbMoneyWindowsServiceError(f"{label} must be absolute") - try: - return path.resolve() - except OSError as error: - raise DumbMoneyWindowsServiceError(f"{label} cannot be resolved") from error - - -def _decode_key_map(value: Any, label: str) -> dict[str, bytes]: - if not isinstance(value, dict) or not 1 <= len(value) <= 16: - raise DumbMoneyWindowsServiceError(f"{label} must contain 1 to 16 public keys") - keys: dict[str, bytes] = {} - for raw_key_id, raw_key in value.items(): - key_id = _digest(raw_key_id, f"{label} key ID") - if not isinstance(raw_key, str): - raise DumbMoneyWindowsServiceError( - f"{label} public-key encoding must be a string" - ) - try: - key = _decode_public_key(raw_key.encode("ascii")) - except (UnicodeEncodeError, DumbMoneyAuthorityError) as error: - raise DumbMoneyWindowsServiceError( - f"{label} public key is invalid" - ) from error - if hashlib.sha256(key).hexdigest() != key_id: - raise DumbMoneyWindowsServiceError( - f"{label} key ID does not bind its public key" - ) - keys[key_id] = key - return keys - - -def load_runner_config( - path: Path, - expected_sha256: str, - program_data: Path, -) -> DopeyRunnerConfig: - """Load only the fixed public config and verify its exact raw bytes.""" - - expected_digest = _digest(expected_sha256, "config_sha256") - fixed_path = (program_data / CONFIG_RELATIVE_PATH).resolve() - if path.resolve() != fixed_path: - raise DumbMoneyWindowsServiceError( - "runner config path is not the fixed ProgramData path" - ) - try: - raw = path.read_bytes() - except OSError as error: - raise DumbMoneyWindowsServiceError( - "pinned public runner config is unavailable" - ) from error - if not 1 <= len(raw) <= MAXIMUM_CONFIG_BYTES: - raise DumbMoneyWindowsServiceError("runner config size is invalid") - if hashlib.sha256(raw).hexdigest() != expected_digest: - raise DumbMoneyWindowsServiceError( - "runner config does not match --config-sha256" - ) - payload = _strict_json(raw, "runner config") - try: - _exact_fields(payload, CONFIG_FIELDS, "Dopey runner config") - except DumbMoneyAuthorityError as error: - raise DumbMoneyWindowsServiceError(str(error)) from error - exact_values = { - "schema": CONFIG_SCHEMA, - "service_name": SERVICE_NAME, - "core_endpoint_ref": CORE_ENDPOINT_REF, - "core_cell_token_target": CELL_TOKEN_TARGET_REF, - "robinhood_profile_target": ROBINHOOD_PROFILE_TARGET_REF, - "start_mode": START_MODE, - "readiness_ref": READINESS_REF, - "codex_local_provider": LOCAL_MODEL_PROVIDER, - } - for field, expected in exact_values.items(): - if payload.get(field) != expected: - raise DumbMoneyWindowsServiceError( - f"runner config {field} is not the sealed value" - ) - release_id = payload.get("release_id") - if not isinstance(release_id, str) or not release_id.strip(): - raise DumbMoneyWindowsServiceError("runner config release_id must be nonempty") - core_key_value = payload.get("core_public_key_base64url") - if not isinstance(core_key_value, str): - raise DumbMoneyWindowsServiceError( - "runner config Core public key must be a string" - ) - try: - core_public_key = _decode_public_key(core_key_value.encode("ascii")) - except (UnicodeEncodeError, DumbMoneyAuthorityError) as error: - raise DumbMoneyWindowsServiceError( - "runner config Core public key is invalid" - ) from error - operator_keys = _decode_key_map( - payload.get("operator_public_keys_base64url"), - "operator", - ) - promoter_keys = _decode_key_map( - payload.get("promoter_public_keys_base64url"), - "promoter", - ) - research_keys = _decode_key_map( - payload.get("research_public_keys_base64url"), - "research", - ) - evaluator_keys = _decode_key_map( - payload.get("evaluator_public_keys_base64url"), - "evaluator", - ) - role_ids = ( - {hashlib.sha256(core_public_key).hexdigest()} - | set(operator_keys) - | set(promoter_keys) - | set(research_keys) - | set(evaluator_keys) - ) - expected_role_count = ( - 1 - + len(operator_keys) - + len(promoter_keys) - + len(research_keys) - + len(evaluator_keys) - ) - if len(role_ids) != expected_role_count: - raise DumbMoneyWindowsServiceError( - "Core/operator/promoter/research public-key roles overlap" - ) - poll_interval = payload.get("poll_interval_seconds") - if ( - isinstance(poll_interval, bool) - or not isinstance(poll_interval, int) - or not 5 <= poll_interval <= 300 - ): - raise DumbMoneyWindowsServiceError( - "poll_interval_seconds must be from 5 through 300" - ) - readiness_ttl = payload.get("readiness_ttl_seconds") - if ( - isinstance(readiness_ttl, bool) - or not isinstance(readiness_ttl, int) - or not 10 <= readiness_ttl <= 120 - ): - raise DumbMoneyWindowsServiceError( - "readiness_ttl_seconds must be from 10 through 120" - ) - local_model = payload.get("codex_local_model") - if ( - not isinstance(local_model, str) - or not LOCAL_MODEL_PATTERN.fullmatch(local_model) - or local_model.endswith((":latest", ":cloud")) - ): - raise DumbMoneyWindowsServiceError( - "codex_local_model must be an explicit non-cloud Ollama tag" - ) - return DopeyRunnerConfig( - release_id=release_id, - core_readiness_path=_exact_path( - payload.get("core_readiness_path"), - program_data / CORE_READINESS_RELATIVE_PATH, - "core_readiness_path", - ), - data_root=_exact_path( - payload.get("data_root"), - program_data / DATA_ROOT_RELATIVE_PATH, - "data_root", - ), - readiness_path=_exact_path( - payload.get("readiness_path"), - program_data / READINESS_RELATIVE_PATH, - "readiness_path", - ), - core_public_key=core_public_key, - operator_public_keys=operator_keys, - promoter_public_keys=promoter_keys, - research_public_keys=research_keys, - evaluator_public_keys=evaluator_keys, - expected_account_hash=_digest( - payload.get("expected_account_hash"), - "expected_account_hash", - ), - fund_lock_sha256=_digest( - payload.get("fund_lock_sha256"), - "fund_lock_sha256", - ), - service_manifest_sha256=_digest( - payload.get("service_manifest_sha256"), - "service_manifest_sha256", - ), - role_public_keys_sha256=_digest( - payload.get("role_public_keys_sha256"), - "role_public_keys_sha256", - ), - core_runner_config_sha256=_digest( - payload.get("core_runner_config_sha256"), - "core_runner_config_sha256", - ), - risk_policy_sha256=_digest( - payload.get("risk_policy_sha256"), - "risk_policy_sha256", - ), - readiness_signer_public_key_sha256=_digest( - payload.get("readiness_signer_public_key_sha256"), - "readiness_signer_public_key_sha256", - ), - codex_executable_path=_absolute_file_path( - payload.get("codex_executable_path"), - "codex_executable_path", - ), - codex_executable_sha256=_digest( - payload.get("codex_executable_sha256"), - "codex_executable_sha256", - ), - codex_local_model=local_model, - codex_local_model_digest=_digest( - payload.get("codex_local_model_digest"), - "codex_local_model_digest", - ), - ollama_executable_path=_absolute_file_path( - payload.get("ollama_executable_path"), - "ollama_executable_path", - ), - ollama_executable_sha256=_digest( - payload.get("ollama_executable_sha256"), - "ollama_executable_sha256", - ), - ollama_process_identity_sid=_windows_sid( - payload.get("ollama_process_identity_sid"), - "ollama_process_identity_sid", - ), - ollama_runtime_evidence_sha256=_digest( - payload.get("ollama_runtime_evidence_sha256"), - "ollama_runtime_evidence_sha256", - ), - poll_interval_seconds=poll_interval, - readiness_ttl_seconds=readiness_ttl, - config_sha256=expected_digest, - ) - - -def _credential_target(reference: str, expected: str) -> str: - if reference != expected or not reference.startswith("credential-target:"): - raise DumbMoneyWindowsServiceError( - "credential reference is not the sealed target" - ) - return reference.removeprefix("credential-target:") - - -def _secret_text(raw: bytes, label: str) -> str: - try: - value = raw.decode("utf-8") - except UnicodeDecodeError as error: - raise DumbMoneyWindowsServiceError( - f"{label} credential is not UTF-8" - ) from error - if ( - not 16 <= len(value) <= 16_384 - or value != value.strip() - or any(character in value for character in "\r\n\0") - ): - raise DumbMoneyWindowsServiceError(f"{label} credential has an invalid shape") - return value - - -def load_secrets( - provider: CredentialProvider, -) -> tuple[str, str]: - """Load exactly two Credential Manager targets with no fallback.""" - - cell_token = _secret_text( - provider.read_bytes( - _credential_target(CELL_TOKEN_TARGET_REF, CELL_TOKEN_TARGET_REF) - ), - CELL_TOKEN_SECRET_REF, - ) - robinhood_profile = _secret_text( - provider.read_bytes( - _credential_target( - ROBINHOOD_PROFILE_TARGET_REF, - ROBINHOOD_PROFILE_TARGET_REF, - ) - ), - ROBINHOOD_PROFILE_SECRET_REF, - ) - return cell_token, robinhood_profile - - -def _readiness_signer(cell_token: str) -> Ed25519PrivateKey: - seed = hashlib.sha256(READINESS_KDF_DOMAIN + cell_token.encode("utf-8")).digest() - return Ed25519PrivateKey.from_private_bytes(seed) - - -def _core_endpoint_from_readiness( - config: DopeyRunnerConfig, - now_utc: datetime, -) -> CoreEndpoint: - try: - raw = config.core_readiness_path.read_bytes() - except OSError as error: - raise DumbMoneyWindowsServiceError( - "signed Core readiness is unavailable" - ) from error - if not 1 <= len(raw) <= MAXIMUM_READINESS_BYTES: - raise DumbMoneyWindowsServiceError("signed Core readiness size is invalid") - envelope = _strict_json(raw, "signed Core readiness") - try: - wrapper = _verify_signed_wrapper( - envelope, - config.core_public_key, - now_utc, - enforce_current_window=True, - ) - except DumbMoneyAuthorityError as error: - raise DumbMoneyWindowsServiceError( - f"signed Core readiness is invalid: {error}" - ) from error - if wrapper.body_schema != CORE_READINESS_SCHEMA: - raise DumbMoneyWindowsServiceError("Core readiness body schema is unsupported") - body = wrapper.body - try: - _exact_fields(body, CORE_READINESS_FIELDS, "Core readiness descriptor") - except DumbMoneyAuthorityError as error: - raise DumbMoneyWindowsServiceError(str(error)) from error - if ( - body.get("schema") != CORE_READINESS_SCHEMA - or body.get("service_name") != "DumbMoneyCore" - or body.get("release_id") != config.release_id - ): - raise DumbMoneyWindowsServiceError( - "Core readiness identity or release binding mismatch" - ) - instance_id = str(body.get("instance_id", "")) - try: - if str(uuid.UUID(instance_id)) != instance_id: - raise ValueError - except (ValueError, AttributeError) as error: - raise DumbMoneyWindowsServiceError( - "Core readiness instance_id is invalid" - ) from error - process_id = body.get("process_id") - generation = body.get("generation") - if ( - isinstance(process_id, bool) - or not isinstance(process_id, int) - or process_id <= 0 - or isinstance(generation, bool) - or not isinstance(generation, int) - or generation < 0 - ): - raise DumbMoneyWindowsServiceError( - "Core readiness process or generation is invalid" - ) - observed_at = str(body.get("observed_at", "")) - valid_until = str(body.get("valid_until", "")) - observed = _utc_timestamp(observed_at, "Core readiness observed_at") - valid = _utc_timestamp(valid_until, "Core readiness valid_until") - now = now_utc.astimezone(timezone.utc) - if ( - not observed <= now < valid - or valid - observed > timedelta(seconds=120) - or wrapper.not_before_utc != observed - or wrapper.expires_at_utc != valid - ): - raise DumbMoneyWindowsServiceError("Core readiness window is invalid or stale") - for field, expected in ( - ("fund_lock_sha256", config.fund_lock_sha256), - ("service_manifest_sha256", config.service_manifest_sha256), - ): - if body.get(field) != expected: - raise DumbMoneyWindowsServiceError(f"Core readiness {field} pin mismatch") - authority = body.get("authority") - if not isinstance(authority, dict) or authority != { - "broker": "NONE", - "mode": "OFFLINE", - "execution_enabled": False, - }: - raise DumbMoneyWindowsServiceError( - "Core readiness claims invalid broker authority" - ) - health = body.get("health") - if not isinstance(health, dict): - raise DumbMoneyWindowsServiceError("Core readiness health must be an object") - health_pins = { - "core_public_key_sha256": hashlib.sha256(config.core_public_key).hexdigest(), - "role_public_keys_sha256": config.role_public_keys_sha256, - "runner_config_sha256": config.core_runner_config_sha256, - "risk_policy_sha256": config.risk_policy_sha256, - } - for field, expected in health_pins.items(): - if health.get(field) != expected: - raise DumbMoneyWindowsServiceError( - f"Core readiness health pin mismatch: {field}" - ) - if health.get("status") not in {"READY", "DEGRADED"}: - raise DumbMoneyWindowsServiceError("Core readiness health status is blocked") - endpoint = body.get("endpoint") - if not isinstance(endpoint, dict) or set(endpoint) != { - "transport", - "host", - "port", - "base_path", - }: - raise DumbMoneyWindowsServiceError("Core readiness endpoint is invalid") - host = endpoint.get("host") - port = endpoint.get("port") - if ( - endpoint.get("transport") != "http" - or host not in {"127.0.0.1", "::1"} - or endpoint.get("base_path") != "/" - or isinstance(port, bool) - or not isinstance(port, int) - or not 1024 <= port <= 65_535 - ): - raise DumbMoneyWindowsServiceError( - "Core readiness endpoint is not literal loopback HTTP" - ) - base_url = f"http://[{host}]:{port}" if host == "::1" else f"http://{host}:{port}" - return CoreEndpoint( - base_url=base_url, - instance_id=instance_id, - observed_at=observed_at, - valid_until=valid_until, - ) - - -def _core_liveness( - transport: CommandFeedTransport, - endpoint: CoreEndpoint, -) -> None: - response = transport( - CommandFeedHttpRequest( - method="GET", - url=f"{endpoint.base_url}/health/live", - headers={"Accept": "application/json"}, - timeout_seconds=5.0, - ) - ) - if ( - not isinstance(response, CommandFeedHttpResponse) - or response.status != 200 - or len(response.body) > 16_384 - ): - raise DumbMoneyWindowsServiceError("Core liveness probe failed") - payload = _strict_json(response.body, "Core liveness") - if payload != {"schema": HEALTH_SCHEMA, "status": "LIVE"}: - raise DumbMoneyWindowsServiceError("Core liveness payload is invalid") - - -def _blocked_dispatch(reason: str) -> DumbMoneyCommandDispatch: - return DumbMoneyCommandDispatch( - desired_mode="PAUSED", - kill_active=True, - allow_new_risk=False, - capital_authority=None, - fail_closed_reason=reason, - required_action="CANCEL_AND_RECONCILE", - caught_up=False, - next_sequence=0, - next_digest="0" * 64, - applied_event_ids=(), - ) - - -def _service_control(config_sha256: str) -> LiveBranchControl: - return LiveBranchControl( - schema_version=CONTROL_SCHEMA, - mode="LIVE", - account_label="agentic", - official_mcp_endpoint=OFFICIAL_ROBINHOOD_MCP, - live_branch_enabled=True, - stock_execution_enabled=True, - options_execution_enabled=False, - paper_twin_required=True, - no_day_trading_enforced=True, - use_full_available_balance=False, - unofficial_broker_api_allowed=False, - owner_authorization_id=f"sealed-service:{config_sha256}", - ) - - -class _HealthServer: - def __init__(self, state_reader: Callable[[], RunnerState]) -> None: - state = state_reader - - class Handler(BaseHTTPRequestHandler): - def do_GET(self) -> None: # noqa: N802 - if self.path == "/health/live": - self._write(200, {"schema": HEALTH_SCHEMA, "status": "LIVE"}) - return - if self.path == "/health/ready": - snapshot = state() - ready = ( - snapshot.health_status == "READY" - and snapshot.reconciled_once - and snapshot.execution_enabled - and snapshot.rollback_anchor_current - ) - self._write( - 200 if ready else 503, - { - "schema": HEALTH_SCHEMA, - "status": "READY" if ready else "NOT_READY", - "reason_codes": [] if ready else [snapshot.reason], - }, - ) - return - self._write(404, {"schema": HEALTH_SCHEMA, "status": "NOT_FOUND"}) - - def _write(self, status: int, payload: dict[str, Any]) -> None: - encoded = canonical_json_bytes(payload) - self.send_response(status) - self.send_header("Content-Type", "application/json") - self.send_header("Cache-Control", "no-store") - self.send_header("Content-Length", str(len(encoded))) - self.end_headers() - self.wfile.write(encoded) - - def log_message(self, _format: str, *_args: Any) -> None: - return - - self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) - self.thread = threading.Thread( - target=self.server.serve_forever, - name="DumbMoneyDopeyRobinhood-health", - daemon=True, - ) - - @property - def port(self) -> int: - return int(self.server.server_address[1]) - - def start(self) -> None: - self.thread.start() - - def close(self) -> None: - self.server.shutdown() - self.server.server_close() - self.thread.join(timeout=5.0) - - -class DumbMoneyDopeyWindowsService: - """Continuous reconcile-first supervisor with an at-most-once write sink.""" - - def __init__( - self, - config: DopeyRunnerConfig, - cell_token: str, - robinhood_profile: str, - *, - transport: CommandFeedTransport, - gateway: RobinhoodAgenticGateway, - cycle_runner: Callable[..., dict[str, Any]] = run_live_branch_cycle, - clock: Callable[[], datetime] = lambda: datetime.now(timezone.utc), - journal_anchor_factory: ( - Callable[[str], CoreJournalAnchorClient] | None - ) = None, - ) -> None: - self.config = config - self.cell_token = cell_token - self.transport = transport - self.gateway = gateway - self.cycle_runner = cycle_runner - self.clock = clock - self._journal_anchor_factory = journal_anchor_factory or ( - lambda base_url: CoreJournalAnchorClient( - base_url=base_url, - transport=self.transport, - cell_token_provider=lambda: self.cell_token, - trusted_core_public_key=self.config.core_public_key, - expected_account_hash=self.config.expected_account_hash, - now_fn=self.clock, - ) - ) - self.instance_id = str(uuid.uuid4()) - self._state = RunnerState() - self._state_lock = threading.Lock() - self._readiness_sequence = 0 - self._signer = _readiness_signer(cell_token) - readiness_public = self._signer.public_key().public_bytes_raw() - if ( - hashlib.sha256(readiness_public).hexdigest() - != config.readiness_signer_public_key_sha256 - ): - raise DumbMoneyWindowsServiceError( - "cell credential does not bind the pinned readiness signer" - ) - if not robinhood_profile: - raise DumbMoneyWindowsServiceError( - "Robinhood Credential Manager profile is absent" - ) - self._health_server = _HealthServer(self.state) - - def state(self) -> RunnerState: - with self._state_lock: - return RunnerState(**self._state.__dict__) - - def _set_state(self, **changes: Any) -> None: - with self._state_lock: - for field, value in changes.items(): - setattr(self._state, field, value) - - def start(self) -> None: - self.config.data_root.mkdir(parents=True, exist_ok=True) - self.config.readiness_path.parent.mkdir(parents=True, exist_ok=True) - self._health_server.start() - self.write_readiness() - - def close(self) -> None: - self._health_server.close() - - def write_readiness(self) -> dict[str, Any]: - now = self.clock().astimezone(timezone.utc) - valid_until = now + timedelta(seconds=self.config.readiness_ttl_seconds) - snapshot = self.state() - self._readiness_sequence += 1 - body: dict[str, Any] = { - "schema": CORE_READINESS_SCHEMA, - "service_name": SERVICE_NAME, - "release_id": self.config.release_id, - "instance_id": self.instance_id, - "process_id": os.getpid(), - "generation": self._readiness_sequence, - "observed_at": _format_utc(now), - "valid_until": _format_utc(valid_until), - "endpoint": { - "transport": "http", - "host": "127.0.0.1", - "port": self._health_server.port, - "base_path": "/", - }, - "fund_lock_sha256": self.config.fund_lock_sha256, - "service_manifest_sha256": self.config.service_manifest_sha256, - "authority": { - "broker": "ROBINHOOD", - "mode": snapshot.mode, - "execution_enabled": snapshot.execution_enabled, - }, - "health": { - "status": snapshot.health_status, - "reason_codes": [snapshot.reason], - "core_ready": snapshot.core_ready, - "reconciled_once": snapshot.reconciled_once, - "last_cycle_status": snapshot.last_cycle_status, - "command_sequence": snapshot.command_sequence, - "command_digest": snapshot.command_digest, - "rollback_anchor_current": snapshot.rollback_anchor_current, - "runner_config_sha256": self.config.config_sha256, - "readiness_public_key_sha256": ( - self.config.readiness_signer_public_key_sha256 - ), - "codex_executable_sha256": (self.config.codex_executable_sha256), - "codex_local_provider": LOCAL_MODEL_PROVIDER, - "codex_local_model": self.config.codex_local_model, - "codex_local_model_digest": (self.config.codex_local_model_digest), - "ollama_executable_sha256": (self.config.ollama_executable_sha256), - "ollama_process_identity_sid": ( - self.config.ollama_process_identity_sid - ), - "ollama_runtime_evidence_sha256": ( - self.config.ollama_runtime_evidence_sha256 - ), - }, - "capabilities": [ - "authenticated-promotion-resolution", - "broker-reconciliation", - "cell-command-checkpoints", - "core-journal-head-anchors", - "single-at-most-once-mcp-boundary", - ], - } - unsigned: dict[str, Any] = { - "schema": SIGNED_ENVELOPE_SCHEMA, - "source_id": SERVICE_NAME, - "source_sequence": self._readiness_sequence, - "correlation_id": f"readiness-{self.instance_id}", - "causation_id": None, - "nonce": hashlib.sha256( - canonical_json_bytes( - { - "instance_id": self.instance_id, - "sequence": self._readiness_sequence, - "observed_at": body["observed_at"], - } - ) - ).hexdigest(), - "not_before": body["observed_at"], - "expires_at": body["valid_until"], - "body_schema": CORE_READINESS_SCHEMA, - "body_digest": hashlib.sha256(canonical_json_bytes(body)).hexdigest(), - "body": body, - "signature_algorithm": "Ed25519", - "signer_key_id": (self.config.readiness_signer_public_key_sha256), - } - unsigned["event_id"] = hashlib.sha256( - canonical_json_bytes(unsigned) - ).hexdigest() - signature = self._signer.sign(canonical_json_bytes(unsigned)) - envelope = { - **unsigned, - "signature": base64.urlsafe_b64encode(signature) - .decode("ascii") - .rstrip("="), - } - _atomic_write_json(self.config.readiness_path, envelope) - return envelope - - def _anchor_local_state(self, client: CoreJournalAnchorClient) -> None: - try: - command_sequence, command_digest = command_feed_head( - self.config.data_root / "control" / "command-feed-state.v1.json" - ) - live_records = read_ledger(ledger_path(self.config.data_root)) - paper_records = read_ledger(paper_twin_path(self.config.data_root)) - except (DumbMoneyCommandFeedError, LiveBranchLedgerError) as error: - raise JournalAnchorError( - "local journal integrity verification failed" - ) from error - heads = ( - ( - "command-feed", - COMMAND_STATE_SCHEMA, - command_sequence, - command_digest, - ), - ( - "live-branch-ledger", - LEDGER_SCHEMA, - len(live_records), - ( - str(live_records[-1]["record_hash"]) - if live_records - else GENESIS_HASH - ), - ), - ( - "paper-twin-ledger", - LEDGER_SCHEMA, - len(paper_records), - ( - str(paper_records[-1]["record_hash"]) - if paper_records - else GENESIS_HASH - ), - ), - ) - for name, schema, sequence, digest in heads: - client.anchor( - journal_name=name, - journal_schema=schema, - journal_sequence=sequence, - journal_head_sha256=digest, - ) - - def run_once(self) -> dict[str, Any]: - now = self.clock().astimezone(timezone.utc) - command_refresh: Callable[[datetime], DumbMoneyCommandDispatch] | None = None - promotion_resolver: DumbMoneyContractResolverClient | None = None - journal_anchor_client: CoreJournalAnchorClient | None = None - core_ready = False - try: - endpoint = _core_endpoint_from_readiness(self.config, now) - _core_liveness(self.transport, endpoint) - journal_anchor_client = self._journal_anchor_factory(endpoint.base_url) - self._anchor_local_state(journal_anchor_client) - command_client = DumbMoneyCommandFeedClient( - base_url=endpoint.base_url, - cell_bearer_provider=lambda: self.cell_token, - trusted_public_key=self.config.core_public_key, - trusted_operator_public_keys=self.config.operator_public_keys, - expected_account_hash=self.config.expected_account_hash, - state_path=( - self.config.data_root / "control" / "command-feed-state.v1.json" - ), - transport=self.transport, - ) - dispatch = command_client.poll_once(now) - self._anchor_local_state(journal_anchor_client) - command_refresh = command_client.poll_once - promotion_resolver = DumbMoneyContractResolverClient( - base_url=endpoint.base_url, - cell_bearer_provider=lambda: self.cell_token, - trusted_core_public_key=self.config.core_public_key, - trusted_promoter_public_keys=self.config.promoter_public_keys, - trusted_research_public_keys=self.config.research_public_keys, - trusted_evaluator_public_keys=self.config.evaluator_public_keys, - transport=self.transport, - ) - core_ready = ( - dispatch.caught_up - and dispatch.required_action == "APPLY_SIGNED_CONTROLS" - ) - except JournalAnchorError as error: - self._set_state( - mode=START_MODE, - health_status="BLOCKED", - reason=f"rollback_anchor_rejected:{type(error).__name__}", - core_ready=False, - reconciled_once=False, - execution_enabled=False, - last_cycle_status="blocked_rollback_anchor", - rollback_anchor_current=False, - ) - self.write_readiness() - return { - "cycle_status": "blocked_rollback_anchor", - "orders": [], - "broker_contacted": False, - } - except Exception as error: - dispatch = _blocked_dispatch( - f"core_readiness_or_transport_failed:{type(error).__name__}" - ) - prior = self.state() - execute_orders = ( - prior.reconciled_once - and prior.rollback_anchor_current - and core_ready - and dispatch.allow_new_risk - and not dispatch.kill_active - and command_refresh is not None - and promotion_resolver is not None - ) - result = self.cycle_runner( - self.config.data_root, - self.gateway, - now, - execute_orders, - dumbmoney_controls=dispatch, - dumbmoney_control_refresh=command_refresh, - dumbmoney_promotion_resolver=promotion_resolver, - authority_clock=self.clock, - live_branch_control=_service_control(self.config.config_sha256), - ) - rollback_anchor_current = False - rollback_anchor_error = "" - if journal_anchor_client is not None: - try: - self._anchor_local_state(journal_anchor_client) - rollback_anchor_current = True - except JournalAnchorError as error: - rollback_anchor_error = ( - f"rollback_anchor_rejected:{type(error).__name__}" - ) - cycle_status = str(result.get("cycle_status", "")) - preflight_reconciled = ( - cycle_status == "preflight_complete" - and not result.get("order_history_blocker") - and isinstance(result.get("account"), dict) - and result["account"].get("authenticated") is True - and result["account"].get("account_hash") - == self.config.expected_account_hash - ) - ambiguous_or_cancel = ( - "ambiguous" in cycle_status - or cycle_status.startswith("cancel_only") - or any( - "ambiguous" in str(order.get("status", "")) - for order in result.get("orders", []) - if isinstance(order, dict) - ) - ) - reconciled = ( - False - if ambiguous_or_cancel - else prior.reconciled_once or preflight_reconciled - ) - execution_enabled = ( - reconciled - and core_ready - and rollback_anchor_current - and dispatch.allow_new_risk - and not dispatch.kill_active - and not ambiguous_or_cancel - ) - healthy_statuses = { - "complete", - "market_closed_monitoring_complete", - "preflight_complete", - } - health_status = ( - "READY" - if execution_enabled and cycle_status in healthy_statuses - else "BLOCKED" - if rollback_anchor_error - else "DEGRADED" - ) - reason = ( - rollback_anchor_error - if rollback_anchor_error - else "ALL_CURRENT_CONTROLS_ALLOW" - if execution_enabled - else dispatch.fail_closed_reason - or cycle_status - or "RECONCILIATION_REQUIRED" - ) - self._set_state( - mode="AGGRESSIVE_BOUNDED" if execution_enabled else START_MODE, - health_status=health_status, - reason=reason, - core_ready=core_ready, - reconciled_once=reconciled, - execution_enabled=execution_enabled, - last_cycle_status=cycle_status or "unknown", - command_sequence=dispatch.next_sequence, - command_digest=dispatch.next_digest, - rollback_anchor_current=rollback_anchor_current, - ) - self.write_readiness() - return result - - def run_forever(self, stop_event: threading.Event) -> None: - self.start() - try: - while not stop_event.is_set(): - if stop_event.wait(self.config.poll_interval_seconds): - break - try: - self.run_once() - except Exception as error: - self._set_state( - mode=START_MODE, - health_status="BLOCKED", - reason=f"cycle_failed_closed:{type(error).__name__}", - core_ready=False, - reconciled_once=False, - execution_enabled=False, - last_cycle_status="cycle_failed_closed", - rollback_anchor_current=False, - ) - self.write_readiness() - finally: - self.close() - - -def _format_utc(value: datetime) -> str: - utc = value.astimezone(timezone.utc) - rendered = utc.isoformat(timespec="microseconds" if utc.microsecond else "seconds") - return rendered.replace("+00:00", "Z") - - -def _atomic_write_json(path: Path, payload: dict[str, Any]) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - temporary = path.with_name(f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp") - encoded = canonical_json_bytes(payload) + b"\n" - try: - with temporary.open("xb") as handle: - handle.write(encoded) - handle.flush() - os.fsync(handle.fileno()) - os.replace(temporary, path) - finally: - try: - temporary.unlink(missing_ok=True) - except OSError: - pass - - -class _DataRootLease: - def __init__(self, path: Path) -> None: - self.path = path - self.handle: Any = None - - def __enter__(self) -> "_DataRootLease": - self.path.parent.mkdir(parents=True, exist_ok=True) - self.handle = self.path.open("a+b") - try: - self.handle.seek(0) - import msvcrt - - msvcrt.locking(self.handle.fileno(), msvcrt.LK_NBLCK, 1) - except (OSError, ImportError) as error: - self.handle.close() - raise DumbMoneyWindowsServiceError( - "another Dopey service owns the ProgramData root" - ) from error - return self - - def __exit__(self, *_args: Any) -> None: - if self.handle is None or self.handle.closed: - return - try: - self.handle.seek(0) - import msvcrt - - msvcrt.locking(self.handle.fileno(), msvcrt.LK_UNLCK, 1) - finally: - self.handle.close() - - -def _program_data_path() -> Path: - value = os.environ.get("PROGRAMDATA", "") - if not value: - raise DumbMoneyWindowsServiceError( - "PROGRAMDATA is required for the sealed Windows service" - ) - path = Path(value).resolve() - if not path.is_absolute(): - raise DumbMoneyWindowsServiceError("PROGRAMDATA must be absolute") - return path - - -def _parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - prog=SERVICE_NAME, - description="Sealed DumbMoney Robinhood venue-cell service", - allow_abbrev=False, - ) - parser.add_argument("--core-endpoint-ref", required=True) - parser.add_argument("--core-cell-token-target", required=True) - parser.add_argument("--robinhood-profile-target", required=True) - parser.add_argument("--start-mode", required=True) - parser.add_argument("--readiness-ref", required=True) - parser.add_argument("--config-sha256", required=True) - return parser - - -def _validate_cli(arguments: argparse.Namespace) -> None: - expected = { - "core_endpoint_ref": CORE_ENDPOINT_REF, - "core_cell_token_target": CELL_TOKEN_TARGET_REF, - "robinhood_profile_target": ROBINHOOD_PROFILE_TARGET_REF, - "start_mode": START_MODE, - "readiness_ref": READINESS_REF, - } - for field, value in expected.items(): - if getattr(arguments, field) != value: - raise DumbMoneyWindowsServiceError( - f"sealed service argument mismatch: {field}" - ) - _digest(arguments.config_sha256, "--config-sha256") - - -def main(argv: list[str] | None = None) -> int: - """Synchronous PyInstaller-compatible service entry point.""" - - try: - arguments = _parser().parse_args(argv) - _validate_cli(arguments) - program_data = _program_data_path() - config_path = (program_data / CONFIG_RELATIVE_PATH).resolve() - config = load_runner_config( - config_path, - arguments.config_sha256, - program_data, - ) - cell_token, robinhood_profile = load_secrets(WindowsCredentialManager()) - parent_mcp = DeterministicRobinhoodMcpClient( - robinhood_profile, - config.expected_account_hash, - ) - gateway = RobinhoodAgenticGateway( - subprocess.run, - 60, - write_timeout_seconds=30, - temporary_root=config.data_root / "temp", - codex_executable_path=config.codex_executable_path, - codex_executable_sha256=config.codex_executable_sha256, - local_model=config.codex_local_model, - local_model_verifier=PinnedOllamaModelVerifier( - config.codex_local_model, - config.codex_local_model_digest, - config.ollama_executable_path, - config.ollama_executable_sha256, - config.ollama_process_identity_sid, - config.data_root / OLLAMA_EVIDENCE_RELATIVE_PATH, - config.ollama_runtime_evidence_sha256, - LoopbackHttpTransport(), - ), - expected_account_hash=config.expected_account_hash, - deterministic_mcp_call=parent_mcp, - ) - service = DumbMoneyDopeyWindowsService( - config, - cell_token, - robinhood_profile, - transport=LoopbackHttpTransport(), - gateway=gateway, - ) - stop_event = threading.Event() - - def stop(_signum: int, _frame: Any) -> None: - stop_event.set() - - for signal_name in ("SIGINT", "SIGTERM"): - candidate = getattr(signal, signal_name, None) - if candidate is not None: - signal.signal(candidate, stop) - with _DataRootLease(config.data_root / "service.lock"): - service.run_forever(stop_event) - return 0 - except KeyboardInterrupt: - return 0 - except (DumbMoneyWindowsServiceError, OSError, ValueError) as error: - print( - f"{SERVICE_NAME} failed closed: {type(error).__name__}", - file=sys.stderr, - ) - return 2 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/dopey_live/ledger.py b/dopey_live/ledger.py index abf02ec..54056f5 100644 --- a/dopey_live/ledger.py +++ b/dopey_live/ledger.py @@ -305,123 +305,6 @@ def reserve_intent( return True -def activate_dumbmoney_fence( - repository_root: Path, - envelope_id: str, - event_id: str, - body_digest: str, - fencing_generation: int, - policy_epoch: int, -) -> dict[str, Any]: - """Persist the highest signed fund-control generation under the ledger lock. - - The existing local process and ledger locks prevent concurrent writers on - one host. This monotonic fence additionally rejects a stale control-plane - process even after it reacquires a local lock. - """ - if fencing_generation <= 0 or policy_epoch <= 0: - raise LiveBranchLedgerError( - "DumbMoney fence generation and policy epoch must be positive" - ) - for label, digest in ( - ("envelope_id", envelope_id), - ("event_id", event_id), - ("body_digest", body_digest), - ): - if not isinstance(digest, str) or len(digest) != 64 or digest.lower() != digest: - raise LiveBranchLedgerError( - f"DumbMoney fence {label} must be lowercase SHA-256 hex" - ) - try: - bytes.fromhex(digest) - except ValueError as error: - raise LiveBranchLedgerError( - f"DumbMoney fence {label} must be lowercase SHA-256 hex" - ) from error - path = ledger_path(repository_root) - with _exclusive_ledger_lock(path): - records = read_ledger(path) - latest: dict[str, Any] | None = None - for record in records: - if record.get("event_type") != "dumbmoney_fence_activated": - continue - payload = record.get("payload") - if isinstance(payload, dict): - latest = payload - if latest is not None: - latest_generation = int(latest.get("fencing_generation", 0)) - latest_epoch = int(latest.get("policy_epoch", 0)) - latest_envelope = str(latest.get("envelope_id", "")) - latest_event = str(latest.get("event_id", "")) - latest_body_digest = str(latest.get("body_digest", "")) - if fencing_generation < latest_generation: - raise LiveBranchLedgerError( - "DumbMoney CapitalEnvelopeV1 is fenced by a newer generation" - ) - if policy_epoch < latest_epoch: - raise LiveBranchLedgerError( - "DumbMoney CapitalEnvelopeV1 is fenced by a newer policy epoch" - ) - if fencing_generation == latest_generation: - if ( - envelope_id != latest_envelope - or event_id != latest_event - or body_digest != latest_body_digest - or policy_epoch != latest_epoch - ): - raise LiveBranchLedgerError( - "DumbMoney fence generation was reused with different authority" - ) - return dict(latest) - payload = { - "envelope_id": envelope_id, - "event_id": event_id, - "body_digest": body_digest, - "fencing_generation": fencing_generation, - "policy_epoch": policy_epoch, - } - _append_record_unlocked(path, "dumbmoney_fence_activated", payload) - return payload - - -def assert_current_dumbmoney_fence( - repository_root: Path, - envelope_id: str, - event_id: str, - body_digest: str, - fencing_generation: int, - policy_epoch: int, -) -> None: - path = ledger_path(repository_root) - with _exclusive_ledger_lock(path): - latest: dict[str, Any] | None = None - for record in read_ledger(path): - if record.get("event_type") != "dumbmoney_fence_activated": - continue - payload = record.get("payload") - if isinstance(payload, dict): - latest = payload - expected = ( - envelope_id, - event_id, - body_digest, - fencing_generation, - policy_epoch, - ) - observed = ( - str(latest.get("envelope_id", "")) if latest else "", - str(latest.get("event_id", "")) if latest else "", - str(latest.get("body_digest", "")) if latest else "", - int(latest.get("fencing_generation", 0)) if latest else 0, - int(latest.get("policy_epoch", 0)) if latest else 0, - ) - if observed != expected: - raise LiveBranchLedgerError( - "DumbMoney authority is no longer the active execution fence: " - f"expected={expected}, observed={observed}" - ) - - def _unresolved_submission_payload( records: tuple[dict[str, Any], ...], intent_id: str, diff --git a/dopey_live/models.py b/dopey_live/models.py index e6e398b..8d917bc 100644 --- a/dopey_live/models.py +++ b/dopey_live/models.py @@ -25,9 +25,6 @@ class AccountSnapshot: equity_order_history_complete: bool option_order_history_complete: bool error: str - # SHA-256 of the broker account identifier. The raw identifier is retained - # only in the in-memory MCP attestation and is never serialized by DOPEY. - account_hash: str = "" def to_dict(self) -> dict[str, Any]: return asdict(self) @@ -86,8 +83,6 @@ class LiveExecutionIntent: execution_snapshot: EquityExecutionSnapshot | None option_contract: dict[str, Any] reason_codes: tuple[str, ...] - promotion_hash: str = "" - passport_hash: str = "" def to_dict(self) -> dict[str, Any]: return asdict(self) @@ -125,12 +120,11 @@ def live_execution_intent_from_dict(payload: dict[str, Any]) -> LiveExecutionInt if not isinstance(raw_option_contract, dict): raise ValueError("Option contract must be an object") migrated["option_contract"] = dict(raw_option_contract) - raw_limit_price = migrated.get( - "limit_price", - raw_option_contract.get("limit_price", 0.0), - ) migrated["limit_price"] = float( - 0.0 if raw_limit_price is None else raw_limit_price + migrated.get( + "limit_price", + raw_option_contract.get("limit_price", 0.0), + ) ) raw_execution_snapshot = migrated.get("execution_snapshot") if raw_execution_snapshot is None: @@ -152,8 +146,6 @@ def live_execution_intent_from_dict(payload: dict[str, Any]) -> LiveExecutionInt migrated["reason_codes"] = tuple( str(value) for value in migrated.get("reason_codes", ()) ) - migrated["promotion_hash"] = str(migrated.get("promotion_hash", "")) - migrated["passport_hash"] = str(migrated.get("passport_hash", "")) return LiveExecutionIntent(**migrated) @@ -173,16 +165,3 @@ class GatewayOrderResult: def to_dict(self) -> dict[str, Any]: return asdict(self) - - -@dataclass(frozen=True) -class GatewayCancelResult: - cancel_requested: bool - order_id: str - status: str - warnings: tuple[str, ...] - error: str - observed_tools: tuple[str, ...] - - def to_dict(self) -> dict[str, Any]: - return asdict(self) diff --git a/dopey_live/robinhood_gateway.py b/dopey_live/robinhood_gateway.py index 9d943f4..33cd208 100644 --- a/dopey_live/robinhood_gateway.py +++ b/dopey_live/robinhood_gateway.py @@ -1,8 +1,7 @@ -"""Robinhood connector with parent-only broker authority and legacy read-only Codex.""" +"""Official Robinhood Trading MCP connector mediated by an ephemeral Codex agent.""" from __future__ import annotations -import hashlib import json import logging import math @@ -11,7 +10,7 @@ import subprocess import tempfile from dataclasses import dataclass -from datetime import date, datetime, timezone +from datetime import date, datetime from decimal import Decimal, InvalidOperation from pathlib import Path from typing import Any, Callable, Final @@ -23,20 +22,12 @@ EQUITY_SNAPSHOT_SOURCE, EquityExecutionSnapshot, EquityQuoteSnapshot, - GatewayCancelResult, GatewayOrderResult, LiveExecutionIntent, ) MCP_SERVER_NAME: Final[str] = "robinhood-trading" -LOCAL_MODEL_PROVIDER: Final[str] = "ollama" -LOCAL_MODEL_BASE_URL: Final[str] = "http://127.0.0.1:11434/v1" -LOCAL_MODEL_PATTERN: Final[re.Pattern[str]] = re.compile( - r"^[a-z0-9][a-z0-9._-]*(?:/[a-z0-9][a-z0-9._-]*)*:" - r"[a-z0-9][a-z0-9._-]{0,63}$" -) -PRE_SUBMIT_SAFETY_MARGIN_SECONDS: Final[int] = 5 SYMBOL_PATTERN = re.compile(r"^[A-Z][A-Z0-9.]{0,9}$") DISABLED_CODEX_FEATURES: Final[tuple[str, ...]] = ( "apps", @@ -69,13 +60,6 @@ "get_portfolio", } ) -DETERMINISTIC_WRITE_TOOLS: Final[frozenset[str]] = frozenset( - { - "cancel_equity_order", - "place_equity_order", - "review_equity_order", - } -) PLACED_ORDER_STATUSES: Final[frozenset[str]] = frozenset( {"queued", "pending", "open", "confirmed", "partially_filled", "filled"} ) @@ -109,7 +93,9 @@ NOTIONAL_FIELDS: Final[frozenset[str]] = frozenset( {"amount", "dollar_amount", "notional", "notional_usd"} ) -LIMIT_PRICE_FIELDS: Final[frozenset[str]] = frozenset({"limit", "limit_price"}) +LIMIT_PRICE_FIELDS: Final[frozenset[str]] = frozenset( + {"limit", "limit_price"} +) ORDER_TYPE_FIELDS: Final[frozenset[str]] = frozenset({"order_type", "type"}) TIME_IN_FORCE_FIELDS: Final[frozenset[str]] = frozenset( {"time_in_force", "timeInForce"} @@ -132,18 +118,6 @@ ) ORDER_ID_FIELDS: Final[frozenset[str]] = frozenset({"order_id"}) ORDER_STATUS_FIELDS: Final[frozenset[str]] = frozenset({"status", "state"}) -CANCEL_ARGUMENT_FIELDS: Final[frozenset[str]] = ( - ACCOUNT_IDENTIFIER_FIELDS | ORDER_ID_FIELDS -) -CANCEL_RESULT_STATUSES: Final[frozenset[str]] = frozenset( - { - *RECOGNIZED_ORDER_STATUSES, - "canceling", - "cancelling", - "cancel_requested", - "not_open", - } -) ORDER_HISTORY_LIMIT: Final[int] = 1_000 LOGGER = logging.getLogger(__name__) Runner = Callable[..., subprocess.CompletedProcess[str]] @@ -157,14 +131,6 @@ class RobinhoodPlacementAmbiguousError(RobinhoodGatewayError): """Raised when an attempted placement cannot be proven placed or not placed.""" -class RobinhoodPreSubmitGuardError(RobinhoodGatewayError): - """Raised when authority is denied before the broker place call begins.""" - - -class RobinhoodCancellationAmbiguousError(RobinhoodGatewayError): - """Raised when an attempted cancellation cannot be proven requested or absent.""" - - class RobinhoodAuthRequiredError(RobinhoodGatewayError): """The MCP server refused the handshake for lack of OAuth - operator must re-login. @@ -458,7 +424,9 @@ def _single_decimal_field( field_label: str, ) -> Decimal: values = _recursive_field_values(value, field_names) - scalar_values = tuple(item for item in values if not isinstance(item, (dict, list))) + scalar_values = tuple( + item for item in values if not isinstance(item, (dict, list)) + ) decimals = {_decimal_value(item, field_label) for item in scalar_values} if len(decimals) != 1: raise RobinhoodGatewayError( @@ -522,9 +490,7 @@ def _agentic_account(calls: tuple[McpToolCallEvidence, ...]) -> AccountAttestati ACCOUNT_TYPE_FIELDS, "agentic_account_type", ) - label = sorted( - labels, key=lambda item: ("agentic" not in item.casefold(), item) - )[0] + label = sorted(labels, key=lambda item: ("agentic" not in item.casefold(), item))[0] matches.append( AccountAttestation( account_id=account_id, @@ -640,24 +606,14 @@ def _field_values_equal(field: str, normalized: Any, raw: Any) -> bool: "average_fill_price", "bid_price", "filled_quantity", - "fees_usd", "last_price", - "limit_price", "market_price", "market_value", - "notional_usd", "quantity", "strike", "unrealized_pnl", } - casefold_fields = { - "option_type", - "order_type", - "side", - "status", - "symbol", - "time_in_force", - } + casefold_fields = {"option_type", "side", "status", "symbol"} if field in numeric_fields: try: return _decimal_value(normalized, field) == _decimal_value(raw, field) @@ -665,7 +621,7 @@ def _field_values_equal(field: str, normalized: Any, raw: Any) -> bool: return False if field in casefold_fields: return str(normalized).strip().casefold() == str(raw).strip().casefold() - return bool(normalized == raw) + return normalized == raw def _bind_normalized_items( @@ -682,9 +638,7 @@ def _bind_normalized_items( ) def identity(item: dict[str, Any]) -> tuple[str, ...]: - values = tuple( - str(item.get(field, "")).strip().casefold() for field in identity_fields - ) + values = tuple(str(item.get(field, "")).strip().casefold() for field in identity_fields) if any(not value for value in values): raise RobinhoodGatewayError( f"Robinhood MCP item identity is incomplete: item={item_label}" @@ -707,13 +661,13 @@ def identity(item: dict[str, Any]) -> tuple[str, ...]: f"Robinhood normalized response contains a duplicate item: item={item_label}" ) observed_normalized.add(normalized_identity) - matched_raw_item = raw_by_identity.get(normalized_identity) - if matched_raw_item is None: + raw_item = raw_by_identity.get(normalized_identity) + if raw_item is None: raise RobinhoodGatewayError( f"Robinhood normalized item is absent from the MCP response: item={item_label}" ) for field in required_fields: - if field not in normalized_item or field not in matched_raw_item: + if field not in normalized_item or field not in raw_item: raise RobinhoodGatewayError( "Robinhood normalized or raw item lacks a required field: " f"item={item_label}, field={field}" @@ -721,7 +675,7 @@ def identity(item: dict[str, Any]) -> tuple[str, ...]: if not _field_values_equal( field, normalized_item[field], - matched_raw_item[field], + raw_item[field], ): raise RobinhoodGatewayError( "Robinhood normalized item differs from the MCP response: " @@ -729,42 +683,6 @@ def identity(item: dict[str, Any]) -> tuple[str, ...]: ) -def _bind_optional_item_fields( - normalized_items: tuple[dict[str, Any], ...], - raw_items: tuple[dict[str, Any], ...], - identity_field: str, - optional_fields: tuple[str, ...], - item_label: str, -) -> None: - raw_by_identity = { - str(item.get(identity_field, "")).strip(): item for item in raw_items - } - for normalized in normalized_items: - identity = str(normalized.get(identity_field, "")).strip() - raw = raw_by_identity.get(identity) - if raw is None: - raise RobinhoodGatewayError( - f"Robinhood optional-field identity is absent: item={item_label}" - ) - for field in optional_fields: - normalized_has = field in normalized - raw_has = field in raw - if normalized_has is not raw_has: - raise RobinhoodGatewayError( - "Robinhood normalized optional economics differ from MCP evidence: " - f"item={item_label}, field={field}" - ) - if normalized_has and not _field_values_equal( - field, - normalized[field], - raw[field], - ): - raise RobinhoodGatewayError( - "Robinhood normalized optional economics differ from MCP evidence: " - f"item={item_label}, field={field}" - ) - - def _attest_order_history_results( calls: tuple[McpToolCallEvidence, ...], ) -> tuple[bool, bool]: @@ -810,7 +728,8 @@ def _attest_order_history_results( frozenset({"next", "next_cursor"}), ) has_continuation = any( - value is not None and str(value).strip() for value in continuation_values + value is not None and str(value).strip() + for value in continuation_values ) has_filter = any( key not in ACCOUNT_IDENTIFIER_FIELDS @@ -856,9 +775,8 @@ def _canonical_raw_broker_item( ), "market_price": raw_item.get("market_price", 0.0), "market_value": raw_item.get("market_value", 0.0), + "unrealized_pnl": raw_item.get("unrealized_pnl", 0.0), } - if "unrealized_pnl" in raw_item: - canonical["unrealized_pnl"] = raw_item["unrealized_pnl"] if item_label == "option_position": canonical["contract_id"] = raw_item.get( "contract_id", @@ -896,19 +814,6 @@ def _canonical_raw_broker_item( _market_date_from_timestamp(filled_at or submitted_at), ), } - optional_aliases = { - "order_type": ("order_type", "type"), - "time_in_force": ("time_in_force", "timeInForce"), - "limit_price": ("limit_price", "limit", "price"), - "quantity": ("quantity", "asset_quantity", "share_quantity", "shares"), - "notional_usd": ("notional_usd", "notional", "amount", "dollar_amount"), - "fees_usd": ("fees_usd", "fees", "total_fees"), - } - for canonical_field, aliases in optional_aliases.items(): - for alias in aliases: - if alias in raw_item: - canonical[canonical_field] = raw_item[alias] - break if item_label == "option_order": canonical.update( { @@ -930,98 +835,6 @@ def _canonical_raw_broker_item( return canonical -def _strict_alias_text( - raw_item: dict[str, Any], - aliases: tuple[str, ...], - field_label: str, -) -> str: - values = [ - str(raw_item[alias]).strip() - for alias in aliases - if alias in raw_item and str(raw_item[alias]).strip() - ] - normalized = {value for value in values} - if len(normalized) != 1: - raise RobinhoodGatewayError( - "Deterministic Robinhood result has a missing or conflicting " - f"text field: field={field_label}" - ) - return next(iter(normalized)) - - -def _strict_alias_number( - raw_item: dict[str, Any], - aliases: tuple[str, ...], - field_label: str, -) -> Any: - values = [ - raw_item[alias] - for alias in aliases - if alias in raw_item and raw_item[alias] is not None - ] - if not values: - raise RobinhoodGatewayError( - f"Deterministic Robinhood result is missing field={field_label}" - ) - decimals = {_decimal_value(value, field_label) for value in values} - if len(decimals) != 1: - raise RobinhoodGatewayError( - "Deterministic Robinhood result has conflicting numeric aliases: " - f"field={field_label}" - ) - return values[0] - - -def _strict_parent_position( - raw_item: dict[str, Any], - *, - option: bool, -) -> dict[str, Any]: - if not isinstance(raw_item, dict): - raise RobinhoodGatewayError( - "Deterministic Robinhood position item must be an object" - ) - canonical: dict[str, Any] = { - "symbol": _strict_alias_text( - raw_item, - ("symbol", "chain_symbol"), - "position.symbol", - ), - "quantity": _strict_alias_number( - raw_item, - ("quantity",), - "position.quantity", - ), - "average_cost": _strict_alias_number( - raw_item, - ("average_cost", "average_buy_price", "average_price"), - "position.average_cost", - ), - "market_price": _strict_alias_number( - raw_item, - ("market_price",), - "position.market_price", - ), - "market_value": _strict_alias_number( - raw_item, - ("market_value",), - "position.market_value", - ), - "unrealized_pnl": _strict_alias_number( - raw_item, - ("unrealized_pnl",), - "position.unrealized_pnl", - ), - } - if option: - canonical["contract_id"] = _strict_alias_text( - raw_item, - ("contract_id", "option_id", "instrument_id"), - "position.contract_id", - ) - return canonical - - def _direct_records_with_fields( value: Any, required_fields: frozenset[str], @@ -1051,9 +864,7 @@ def _requested_symbols(arguments: dict[str, Any]) -> frozenset[str]: raise RobinhoodGatewayError( "Robinhood quote request symbol arguments must be strings" ) - symbols.update( - str(value).strip().upper() for value in values if str(value).strip() - ) + symbols.update(str(value).strip().upper() for value in values if str(value).strip()) if not symbols: raise RobinhoodGatewayError( "Robinhood quote request does not expose requested symbols" @@ -1087,7 +898,9 @@ def _attest_equity_execution_snapshot( ) quote_records = _direct_records_with_fields( _mcp_result_value(quote_calls[0]), - frozenset({"symbol", "bid_price", "ask_price", "last_price", "observed_at"}), + frozenset( + {"symbol", "bid_price", "ask_price", "last_price", "observed_at"} + ), ) _bind_normalized_items( normalized_quotes, @@ -1251,20 +1064,6 @@ def _validated_order(item: Any, index: int, option: bool) -> dict[str, Any]: raise RobinhoodGatewayError( f"Robinhood filled order average price must be positive: item={item_label}" ) - for field in ("limit_price", "quantity", "notional_usd", "fees_usd"): - if field in item: - value = _required_finite_item_number(item, field, item_label, True) - if value < 0.0: - raise RobinhoodGatewayError( - f"Robinhood broker order economics must be nonnegative: " - f"item={item_label}, field={field}" - ) - for field in ("order_type", "time_in_force"): - if field in item and not str(item.get(field, "")).strip(): - raise RobinhoodGatewayError( - f"Robinhood broker order field must be nonempty: " - f"item={item_label}, field={field}" - ) try: date.fromisoformat(str(item.get("market_date", ""))) except ValueError as error: @@ -1392,26 +1191,18 @@ def _attest_exact_order_fields(value: Any, intent: LiveExecutionIntent) -> None: def _attest_order_call( call: McpToolCallEvidence, intent: LiveExecutionIntent, - account_id: str | None, + account_id: str, ) -> dict[str, Any] | list[Any]: _attest_order_argument_allowlist(call.arguments, call.tool) - if account_id is None: - if _recursive_field_values(call.arguments, ACCOUNT_IDENTIFIER_FIELDS): - raise RobinhoodGatewayError( - "Deterministic Robinhood order call unexpectedly supplied an " - f"account identifier: tool={call.tool}" - ) - else: - observed_account_id = _single_string_field( - call.arguments, - ACCOUNT_IDENTIFIER_FIELDS, - f"{call.tool}_account_id", + observed_account_id = _single_string_field( + call.arguments, + ACCOUNT_IDENTIFIER_FIELDS, + f"{call.tool}_account_id", + ) + if observed_account_id != account_id: + raise RobinhoodGatewayError( + f"Robinhood order call is not bound to the labeled Agentic account: tool={call.tool}" ) - if observed_account_id != account_id: - raise RobinhoodGatewayError( - "Robinhood order call is not bound to the labeled Agentic " - f"account: tool={call.tool}" - ) _attest_exact_order_fields(call.arguments, intent) result_value = _mcp_result_value(call) _attest_exact_order_fields(result_value, intent) @@ -1484,20 +1275,9 @@ def _codex_exec_command( working_directory: str, result_path: Path, allowed_tools: tuple[str, ...], - bearer_token_env_var: str | None = None, - local_model: str | None = None, ) -> list[str]: if not allowed_tools: raise ValueError("allowed_tools must contain at least one MCP tool") - if bearer_token_env_var is not None: - raise ValueError( - "Broker bearer material is prohibited from generative subprocesses" - ) - if local_model is not None and ( - not LOCAL_MODEL_PATTERN.fullmatch(local_model) - or local_model.endswith((":latest", ":cloud")) - ): - raise ValueError("local_model must be an explicit non-cloud Ollama tag") enabled_tools = json.dumps( sorted(set(allowed_tools)), ensure_ascii=True, @@ -1516,47 +1296,20 @@ def _codex_exec_command( "--color", "never", "--json", + "-c", + f'mcp_servers.{MCP_SERVER_NAME}.url="{OFFICIAL_ROBINHOOD_MCP}"', + "-c", + f"mcp_servers.{MCP_SERVER_NAME}.enabled_tools={enabled_tools}", + "-c", + f'mcp_servers.{MCP_SERVER_NAME}.default_tools_approval_mode="writes"', ] - if local_model is not None: - command.extend( - ( - "--oss", - "--local-provider", - LOCAL_MODEL_PROVIDER, - "--model", - local_model, - "-c", - ( - f"model_providers.{LOCAL_MODEL_PROVIDER}.base_url=" - f'"{LOCAL_MODEL_BASE_URL}"' - ), - ) - ) - if local_model is None: + for tool in sorted(set(allowed_tools) & READ_ONLY_ROBINHOOD_TOOLS): command.extend( ( "-c", - f'mcp_servers.{MCP_SERVER_NAME}.url="{OFFICIAL_ROBINHOOD_MCP}"', + f'mcp_servers.{MCP_SERVER_NAME}.tools.{tool}.approval_mode="approve"', ) ) - command.extend( - ( - "-c", - f"mcp_servers.{MCP_SERVER_NAME}.enabled_tools={enabled_tools}", - "-c", - f'mcp_servers.{MCP_SERVER_NAME}.default_tools_approval_mode="writes"', - ) - ) - for tool in sorted(set(allowed_tools) & READ_ONLY_ROBINHOOD_TOOLS): - command.extend( - ( - "-c", - ( - f"mcp_servers.{MCP_SERVER_NAME}.tools." - f'{tool}.approval_mode="approve"' - ), - ) - ) for feature in DISABLED_CODEX_FEATURES: command.extend(("--disable", feature)) command.extend( @@ -1571,93 +1324,6 @@ def _codex_exec_command( return command -def _direct_call_evidence( - tool: str, - arguments: dict[str, Any], - raw_result: dict[str, Any], -) -> McpToolCallEvidence: - if raw_result.get("isError") is True or raw_result.get("is_error") is True: - raise RobinhoodGatewayError( - f"Deterministic Robinhood MCP tool returned an error: tool={tool}" - ) - structured = raw_result.get( - "structuredContent", - raw_result.get("structured_content"), - ) - content = raw_result.get("content") - normalized: dict[str, Any] = {} - if isinstance(structured, (dict, list)): - normalized["structured_content"] = structured - if isinstance(content, list): - normalized["content"] = content - if not normalized: - raise RobinhoodGatewayError( - f"Deterministic Robinhood MCP result lacks typed evidence: tool={tool}" - ) - return McpToolCallEvidence( - call_id=f"deterministic-{tool}", - tool=tool, - arguments=dict(arguments), - result=normalized, - ) - - -def _direct_result_warnings(value: Any) -> tuple[str, ...]: - warnings: list[str] = [] - for item in _recursive_field_values( - value, - frozenset({"alerts", "warning", "warnings"}), - ): - if isinstance(item, str): - warnings.append(item) - elif isinstance(item, list) and all(isinstance(entry, str) for entry in item): - warnings.extend(item) - else: - raise RobinhoodGatewayError( - "Deterministic Robinhood MCP warnings are not typed strings" - ) - if len(warnings) > 64 or any(len(warning) > 2_048 for warning in warnings): - raise RobinhoodGatewayError( - "Deterministic Robinhood MCP warnings exceed bounded limits" - ) - return tuple(warnings) - - -def _optional_direct_decimal( - value: Any, - field_names: frozenset[str], - field_label: str, -) -> float: - values = _recursive_field_values(value, field_names) - scalar_values = tuple(item for item in values if not isinstance(item, (dict, list))) - if not scalar_values: - return 0.0 - decimals = {_decimal_value(item, field_label) for item in scalar_values} - if len(decimals) != 1: - raise RobinhoodGatewayError( - "Deterministic Robinhood MCP result contains an ambiguous number: " - f"field={field_label}" - ) - return float(next(iter(decimals))) - - -def _optional_direct_string( - value: Any, - field_names: frozenset[str], - field_label: str, -) -> str: - values = _recursive_field_values(value, field_names) - normalized = {str(item).strip() for item in values if str(item).strip()} - if not normalized: - return "" - if len(normalized) != 1: - raise RobinhoodGatewayError( - "Deterministic Robinhood MCP result contains ambiguous text: " - f"field={field_label}" - ) - return next(iter(normalized)) - - def _gateway_order_result( payload: dict[str, Any], review_tool: str, @@ -1872,695 +1538,20 @@ def _gateway_order_result( ) -def _gateway_cancel_result( - payload: dict[str, Any], - cancel_tool: str, - expected_order_id: str, - calls: tuple[McpToolCallEvidence, ...], - account_id: str, -) -> GatewayCancelResult: - warnings = payload.get("warnings") - if not isinstance(warnings, list) or not all( - isinstance(value, str) for value in warnings - ): - raise RobinhoodCancellationAmbiguousError( - "Robinhood Agentic cancellation warnings must be an array of strings" - ) - cancel_calls = _tool_calls(calls, cancel_tool) - if len(cancel_calls) > 1: - raise RobinhoodCancellationAmbiguousError( - "Robinhood MCP transcript contains duplicate cancellation calls" - ) - cancel_requested = payload.get("cancel_requested") - if type(cancel_requested) is not bool: - raise RobinhoodCancellationAmbiguousError( - "Robinhood cancellation result must contain boolean cancel_requested" - ) - order_id = str(payload.get("order_id", "")).strip() - status = str(payload.get("status", "")).strip().lower() - error_text = str(payload.get("error", "")).strip() - if order_id and order_id != expected_order_id: - raise RobinhoodCancellationAmbiguousError( - "Robinhood cancellation result references a different order" - ) - if status not in CANCEL_RESULT_STATUSES: - raise RobinhoodCancellationAmbiguousError( - f"Robinhood cancellation status is unrecognized: status={status!r}" - ) - if cancel_calls: - call = cancel_calls[0] - unexpected = sorted(set(call.arguments) - set(CANCEL_ARGUMENT_FIELDS)) - if unexpected: - raise RobinhoodCancellationAmbiguousError( - "Robinhood cancellation call contains unexpected arguments: " - f"unexpected={unexpected}" - ) - observed_account_id = _single_string_field( - call.arguments, - ACCOUNT_IDENTIFIER_FIELDS, - "cancel_account_id", - ) - observed_order_id = _single_string_field( - call.arguments, - ORDER_ID_FIELDS, - "cancel_order_id", - ) - if observed_account_id != account_id or observed_order_id != expected_order_id: - raise RobinhoodCancellationAmbiguousError( - "Robinhood cancellation call is not bound to the exact Agentic order" - ) - result_value = _mcp_result_value(call) - transcript_order_id = _single_string_field( - result_value, - ORDER_ID_FIELDS, - "cancel_result_order_id", - ) - transcript_status = _single_string_field( - result_value, - ORDER_STATUS_FIELDS, - "cancel_result_status", - ).lower() - if ( - transcript_order_id != expected_order_id - or transcript_status != status - or not cancel_requested - ): - raise RobinhoodCancellationAmbiguousError( - "Robinhood normalized cancellation result is not bound to the MCP transcript" - ) - elif cancel_requested: - raise RobinhoodCancellationAmbiguousError( - "Robinhood reported a cancellation request without a cancellation tool call" - ) - if cancel_requested and (not order_id or error_text): - raise RobinhoodCancellationAmbiguousError( - "Robinhood accepted cancellation with contradictory normalized fields" - ) - return GatewayCancelResult( - cancel_requested=cancel_requested, - order_id=order_id, - status=status, - warnings=tuple(warnings), - error=error_text, - observed_tools=tuple(sorted({call.tool for call in calls})), - ) - - class RobinhoodAgenticGateway: """Connector class; trading decisions remain in pure upstream functions.""" - def __init__( - self, - runner: Runner, - timeout_seconds: int, - *, - write_timeout_seconds: int | None = None, - mcp_bearer_token: str | None = None, - temporary_root: Path | None = None, - codex_executable_path: Path | None = None, - codex_executable_sha256: str | None = None, - local_model: str | None = None, - local_model_verifier: Callable[[], None] | None = None, - expected_account_hash: str | None = None, - deterministic_mcp_call: ( - Callable[ - [str, dict[str, Any], int, Callable[[], None] | None], - dict[str, Any], - ] - | None - ) = None, - ) -> None: + def __init__(self, runner: Runner, timeout_seconds: int) -> None: if timeout_seconds <= 0: raise ValueError(f"timeout_seconds must be positive: {timeout_seconds}") - if write_timeout_seconds is None: - write_timeout_seconds = min(30, timeout_seconds) - if ( - isinstance(write_timeout_seconds, bool) - or not isinstance(write_timeout_seconds, int) - or write_timeout_seconds <= 0 - or write_timeout_seconds > timeout_seconds - ): - raise ValueError( - "write_timeout_seconds must be a positive integer no greater " - "than timeout_seconds" - ) - if mcp_bearer_token is not None: - raise ValueError( - "Broker bearer material is prohibited from the generative gateway; " - "use the deterministic parent-side MCP transport" - ) - if deterministic_mcp_call is not None and ( - not isinstance(expected_account_hash, str) - or not re.fullmatch(r"[0-9a-f]{64}", expected_account_hash) - ): - raise ValueError( - "Deterministic parent-side MCP requires the pinned account hash" - ) - if deterministic_mcp_call is None and expected_account_hash is not None: - raise ValueError( - "expected_account_hash requires deterministic parent-side MCP" - ) - if local_model is not None and ( - not LOCAL_MODEL_PATTERN.fullmatch(local_model) - or local_model.endswith((":latest", ":cloud")) - ): - raise ValueError("local_model must be an explicit non-cloud Ollama tag") - if (local_model is None) != (local_model_verifier is None): - raise ValueError( - "local_model and local_model_verifier must be supplied together" + executable = shutil.which("codex") + if executable is None: + raise RobinhoodGatewayError( + "Codex CLI is required for the Robinhood Trading MCP gateway" ) - if codex_executable_path is None: - if codex_executable_sha256 is not None: - raise ValueError( - "codex_executable_sha256 requires codex_executable_path" - ) - if deterministic_mcp_call is not None: - executable = "" - else: - discovered_executable = shutil.which("codex") - if discovered_executable is None: - raise RobinhoodGatewayError( - "Codex CLI is required for the Robinhood Trading MCP gateway" - ) - executable = discovered_executable - self._codex_sha256: str | None = None - else: - if not codex_executable_path.is_absolute(): - raise ValueError("Codex executable path must be absolute") - executable_path = codex_executable_path.resolve() - if not executable_path.is_file(): - raise RobinhoodGatewayError( - "Pinned Codex CLI executable is unavailable" - ) - if not isinstance(codex_executable_sha256, str) or not re.fullmatch( - r"[0-9a-f]{64}", codex_executable_sha256 - ): - raise ValueError("Pinned Codex executable requires lowercase SHA-256") - executable = str(executable_path) - self._codex_sha256 = codex_executable_sha256 self._runner = runner self._timeout_seconds = timeout_seconds - self._write_timeout_seconds = write_timeout_seconds self._codex = executable - self._verify_codex_executable() - self._local_model = local_model - self._local_model_verifier = local_model_verifier - self._deterministic_mcp_call = deterministic_mcp_call - self._parent_only_mode = deterministic_mcp_call is not None - self._expected_account_hash = expected_account_hash - self._temporary_root = ( - temporary_root.resolve() if temporary_root is not None else None - ) - if self._temporary_root is not None: - self._temporary_root.mkdir(parents=True, exist_ok=True) - self._bearer_token_env_var: str | None = None - self._subprocess_env: dict[str, str] | None = None - - def _verify_codex_executable(self) -> None: - if self._codex_sha256 is None: - return - digest = hashlib.sha256() - try: - with Path(self._codex).open("rb") as handle: - for chunk in iter(lambda: handle.read(1024 * 1024), b""): - digest.update(chunk) - except OSError as error: - raise RobinhoodGatewayError( - "Pinned Codex CLI executable cannot be read" - ) from error - if digest.hexdigest() != self._codex_sha256: - raise RobinhoodGatewayError("Pinned Codex CLI executable SHA-256 mismatch") - - def _verify_local_model(self) -> None: - if self._local_model_verifier is None: - return - try: - self._local_model_verifier() - except Exception as error: - raise RobinhoodGatewayError( - "Pinned local Ollama model verification failed" - ) from error - - def _call_deterministic_mcp( - self, - tool: str, - arguments: dict[str, Any], - timeout_seconds: int, - *, - pre_call_guard: Callable[[], None] | None = None, - ) -> McpToolCallEvidence: - if ( - self._deterministic_mcp_call is None - or tool not in DETERMINISTIC_WRITE_TOOLS - ): - raise RobinhoodGatewayError( - "Deterministic Robinhood MCP write transport is unavailable" - ) - try: - raw_result = self._deterministic_mcp_call( - tool, - dict(arguments), - timeout_seconds, - pre_call_guard, - ) - except RobinhoodGatewayError: - raise - except Exception as error: - raise RobinhoodGatewayError( - "Deterministic Robinhood MCP call failed: " - f"tool={tool}, error={type(error).__name__}" - ) from error - if not isinstance(raw_result, dict): - raise RobinhoodGatewayError( - f"Deterministic Robinhood MCP result is not an object: tool={tool}" - ) - return _direct_call_evidence(tool, arguments, raw_result) - - def _call_deterministic_read( - self, - tool: str, - arguments: dict[str, Any], - ) -> McpToolCallEvidence: - if ( - self._deterministic_mcp_call is None - or tool not in READ_ONLY_ROBINHOOD_TOOLS - ): - raise RobinhoodGatewayError( - "Deterministic parent-side Robinhood read transport is unavailable" - ) - try: - raw_result = self._deterministic_mcp_call( - tool, - dict(arguments), - self._timeout_seconds, - None, - ) - except RobinhoodGatewayError: - raise - except Exception as error: - raise RobinhoodGatewayError( - "Deterministic parent-side Robinhood read failed: " - f"tool={tool}, error={type(error).__name__}" - ) from error - if not isinstance(raw_result, dict): - raise RobinhoodGatewayError( - f"Deterministic Robinhood read result is not an object: tool={tool}" - ) - return _direct_call_evidence(tool, arguments, raw_result) - - def _get_account_snapshot_deterministically(self) -> AccountSnapshot: - calls: list[McpToolCallEvidence] = [ - self._call_deterministic_read("get_accounts", {}) - ] - account = _agentic_account(tuple(calls)) - observed_account_hash = hashlib.sha256( - account.account_id.encode("utf-8") - ).hexdigest() - if observed_account_hash != self._expected_account_hash: - raise RobinhoodGatewayError( - "Deterministic Robinhood read is not bound to the pinned " - "Agentic account" - ) - account_arguments = {"account_number": account.account_id} - for tool in ( - "get_portfolio", - "get_equity_positions", - "get_option_positions", - ): - calls.append(self._call_deterministic_read(tool, account_arguments)) - for tool in ("get_equity_orders", "get_option_orders"): - calls.append( - self._call_deterministic_read( - tool, - { - **account_arguments, - "limit": ORDER_HISTORY_LIMIT, - }, - ) - ) - evidence = tuple(calls) - attested_account = _attest_account_scope( - evidence, - ( - "get_portfolio", - "get_equity_positions", - "get_option_positions", - "get_equity_orders", - "get_option_orders", - ), - ) - portfolio = _mcp_result_value(_tool_calls(evidence, "get_portfolio")[0]) - buying_power = float( - _single_decimal_field( - portfolio, - frozenset({"buying_power", "buying_power_usd"}), - "buying_power_usd", - ) - ) - total_equity = float( - _single_decimal_field( - portfolio, - frozenset( - { - "equity", - "portfolio_value", - "total_equity", - "total_equity_usd", - "total_value", - } - ), - "total_equity_usd", - ) - ) - if ( - not math.isfinite(buying_power) - or buying_power < 0.0 - or not math.isfinite(total_equity) - or total_equity < 0.0 - ): - raise RobinhoodGatewayError( - "Deterministic Robinhood portfolio values must be finite " - "and nonnegative" - ) - equity_positions = tuple( - _validated_equity_position( - _strict_parent_position(item, option=False), - index, - ) - for index, item in enumerate( - _result_items( - _tool_calls(evidence, "get_equity_positions")[0], - frozenset({"equity_positions", "positions", "results"}), - "equity_position", - ) - ) - ) - option_positions = tuple( - _validated_option_position( - _strict_parent_position(item, option=True), - index, - ) - for index, item in enumerate( - _result_items( - _tool_calls(evidence, "get_option_positions")[0], - frozenset({"option_positions", "positions", "results"}), - "option_position", - ) - ) - ) - equity_orders = tuple( - _validated_order( - _canonical_raw_broker_item(item, "equity_order"), - index, - False, - ) - for index, item in enumerate( - _result_items( - _tool_calls(evidence, "get_equity_orders")[0], - frozenset({"equity_orders", "orders", "results"}), - "equity_order", - ) - ) - ) - option_orders = tuple( - _validated_order( - _canonical_raw_broker_item(item, "option_order"), - index, - True, - ) - for index, item in enumerate( - _result_items( - _tool_calls(evidence, "get_option_orders")[0], - frozenset({"option_orders", "orders", "results"}), - "option_order", - ) - ) - ) - equity_complete, option_complete = _attest_order_history_results(evidence) - errors: list[str] = [] - if not equity_complete: - errors.append("equity_order_history_incomplete") - if not option_complete: - errors.append("option_order_history_incomplete") - return AccountSnapshot( - authenticated=True, - account_type=attested_account.account_type, - buying_power_usd=buying_power, - total_equity_usd=total_equity, - equity_positions=equity_positions, - option_positions=option_positions, - recent_equity_orders=equity_orders, - recent_option_orders=option_orders, - equity_order_history_complete=equity_complete, - option_order_history_complete=option_complete, - error=";".join(errors), - account_hash=observed_account_hash, - ) - - def _get_equity_snapshot_deterministically( - self, - intent: LiveExecutionIntent, - ) -> EquityExecutionSnapshot: - expected_symbols = frozenset({intent.symbol, intent.analysis_symbol}) - arguments = {"symbols": sorted(expected_symbols)} - quote_call = self._call_deterministic_read( - "get_equity_quotes", - arguments, - ) - tradability_call = self._call_deterministic_read( - "get_equity_tradability", - arguments, - ) - quote_records = _direct_records_with_fields( - _mcp_result_value(quote_call), - frozenset( - { - "symbol", - "bid_price", - "ask_price", - "last_price", - "observed_at", - } - ), - ) - tradability_records = _direct_records_with_fields( - _mcp_result_value(tradability_call), - frozenset({"symbol", "tradable"}), - ) - quotes_by_symbol = { - str(item["symbol"]).strip().upper(): item for item in quote_records - } - tradability_by_symbol = { - str(item["symbol"]).strip().upper(): item for item in tradability_records - } - if ( - len(quotes_by_symbol) != len(quote_records) - or len(tradability_by_symbol) != len(tradability_records) - or frozenset(quotes_by_symbol) != expected_symbols - or frozenset(tradability_by_symbol) != expected_symbols - ): - raise RobinhoodGatewayError( - "Deterministic Robinhood quote response is missing, duplicated, " - "or contains an unrequested symbol" - ) - - def quote(symbol: str) -> EquityQuoteSnapshot: - raw_quote = quotes_by_symbol[symbol] - raw_tradability = tradability_by_symbol[symbol] - _aware_timestamp( - raw_quote["observed_at"], - "observed_at", - f"equity_quote[{symbol}]", - False, - ) - return _equity_quote_snapshot( - { - "quote": { - "symbol": symbol, - "bid_price": raw_quote["bid_price"], - "ask_price": raw_quote["ask_price"], - "last_price": raw_quote["last_price"], - "observed_at": raw_quote["observed_at"], - "tradable": raw_tradability["tradable"], - } - }, - "quote", - symbol, - ) - - snapshot = EquityExecutionSnapshot( - execution_quote=quote(intent.symbol), - analysis_quote=quote(intent.analysis_symbol), - source=EQUITY_SNAPSHOT_SOURCE, - ) - _attest_equity_execution_snapshot( - (quote_call, tradability_call), - intent, - snapshot, - ) - return snapshot - - @staticmethod - def _deterministic_equity_arguments( - intent: LiveExecutionIntent, - ) -> dict[str, Any]: - if intent.instrument != "equity": - raise RobinhoodGatewayError( - "Deterministic live execution is currently equity-only" - ) - if not math.isfinite(intent.quantity) or intent.quantity <= 0.0: - raise RobinhoodGatewayError( - "Deterministic equity execution requires an exact positive quantity" - ) - arguments: dict[str, Any] = { - "side": intent.side, - "symbol": intent.symbol, - "quantity": intent.quantity, - "order_type": intent.order_type, - "time_in_force": intent.time_in_force, - } - if intent.order_type == "limit": - arguments["limit_price"] = intent.limit_price - return arguments - - def _place_equity_order_deterministically( - self, - intent: LiveExecutionIntent, - pre_submit_guard: Callable[[], datetime] | None, - ) -> GatewayOrderResult: - arguments = self._deterministic_equity_arguments(intent) - review_call = self._call_deterministic_mcp( - "review_equity_order", - arguments, - self._write_timeout_seconds, - ) - review_result = _attest_order_call(review_call, intent, None) - review_succeeded = _single_bool_field( - review_result, - frozenset({"approved", "review_succeeded"}), - "review_succeeded", - ) - warnings = _direct_result_warnings(review_result) - if not review_succeeded: - return GatewayOrderResult( - review_succeeded=False, - placed=False, - order_id="", - status="blocked_by_broker_review", - filled_quantity=0.0, - average_fill_price=0.0, - submitted_at="", - filled_at="", - warnings=warnings, - error="broker_review_denied", - observed_tools=("review_equity_order",), - ) - try: - place_call = self._call_deterministic_mcp( - "place_equity_order", - arguments, - self._write_timeout_seconds, - pre_call_guard=( - None - if pre_submit_guard is None - else lambda: self._validate_pre_submit_guard(pre_submit_guard) - ), - ) - place_result = _attest_order_call(place_call, intent, None) - placed = _single_bool_field( - place_result, - frozenset({"placed"}), - "placed", - ) - order_id = _single_string_field( - place_result, - ORDER_ID_FIELDS, - "order_id", - ) - status = _single_string_field( - place_result, - ORDER_STATUS_FIELDS, - "status", - ).lower() - if not placed or status not in PLACED_ORDER_STATUSES: - raise RobinhoodGatewayError( - "Deterministic Robinhood placement result is not a " - "recognized placed order" - ) - return GatewayOrderResult( - review_succeeded=True, - placed=True, - order_id=order_id, - status=status, - filled_quantity=_optional_direct_decimal( - place_result, - frozenset({"filled_quantity"}), - "filled_quantity", - ), - average_fill_price=_optional_direct_decimal( - place_result, - frozenset({"average_fill_price"}), - "average_fill_price", - ), - submitted_at=_optional_direct_string( - place_result, - frozenset({"submitted_at"}), - "submitted_at", - ), - filled_at=_optional_direct_string( - place_result, - frozenset({"filled_at"}), - "filled_at", - ), - warnings=tuple( - dict.fromkeys((*warnings, *_direct_result_warnings(place_result))) - ), - error="", - observed_tools=( - "place_equity_order", - "review_equity_order", - ), - ) - except RobinhoodPreSubmitGuardError: - raise - except RobinhoodGatewayError as error: - raise RobinhoodPlacementAmbiguousError( - "Deterministic Robinhood placement has an unknown broker " - "outcome and must not be retried" - ) from error - - def _validate_pre_submit_guard( - self, - pre_submit_guard: Callable[[], datetime], - ) -> None: - try: - authority_deadline = pre_submit_guard() - except RobinhoodPreSubmitGuardError: - raise - except Exception as error: - raise RobinhoodPreSubmitGuardError( - "Final broker-sink pre-submit guard denied the write: " - f"{type(error).__name__}: {error}" - ) from error - if ( - not isinstance(authority_deadline, datetime) - or authority_deadline.tzinfo is None - or authority_deadline.utcoffset() is None - ): - raise RobinhoodPreSubmitGuardError( - "Final broker-sink guard must return an aware authority deadline" - ) - remaining_seconds = ( - authority_deadline.astimezone(timezone.utc) - datetime.now(timezone.utc) - ).total_seconds() - required_seconds = ( - self._write_timeout_seconds + PRE_SUBMIT_SAFETY_MARGIN_SECONDS - ) - if remaining_seconds <= required_seconds: - raise RobinhoodPreSubmitGuardError( - "Capital authority has insufficient remaining validity for " - "the bounded external write operation" - ) def _run( self, @@ -2569,37 +1560,13 @@ def _run( required_tools: tuple[str, ...], allowed_tools: tuple[str, ...], attempt_limit: int, - timeout_seconds: int | None = None, ) -> GatewayEnvelope: - if self._parent_only_mode: - raise RobinhoodGatewayError( - "Parent-only broker mode prohibits generative MCP fallback" - ) if attempt_limit <= 0: raise ValueError(f"attempt_limit must be positive: {attempt_limit}") - if self._bearer_token_env_var is not None and not set(allowed_tools).issubset( - READ_ONLY_ROBINHOOD_TOOLS - ): - forbidden = sorted(set(allowed_tools) - READ_ONLY_ROBINHOOD_TOOLS) - raise RobinhoodGatewayError( - "Credential-bound model execution may expose only read-only " - f"Robinhood tools: forbidden={forbidden}" - ) - effective_timeout = ( - self._timeout_seconds if timeout_seconds is None else timeout_seconds - ) - if effective_timeout <= 0 or effective_timeout > self._timeout_seconds: - raise ValueError( - "operation timeout must be positive and no greater than " - "the gateway timeout" - ) last_error = "" for attempt in range(1, attempt_limit + 1): - self._verify_codex_executable() - self._verify_local_model() with tempfile.TemporaryDirectory( - prefix="dopey-robinhood-agentic-", - dir=self._temporary_root, + prefix="dopey-robinhood-agentic-" ) as temporary: result_path = Path(temporary) / "result.json" command = _codex_exec_command( @@ -2607,8 +1574,6 @@ def _run( temporary, result_path, allowed_tools, - self._bearer_token_env_var, - self._local_model, ) try: completed = self._runner( @@ -2618,9 +1583,8 @@ def _run( encoding="utf-8", errors="strict", capture_output=True, - timeout=effective_timeout, + timeout=self._timeout_seconds, check=False, - env=self._subprocess_env, ) except (OSError, subprocess.TimeoutExpired) as error: last_error = f"{type(error).__name__}: {error}" @@ -2749,8 +1713,6 @@ def _run( ) def get_account_snapshot(self) -> AccountSnapshot: - if getattr(self, "_deterministic_mcp_call", None) is not None: - return self._get_account_snapshot_deterministically() prompt = """Standing owner authorization applies. Use only Robinhood Trading MCP tools get_accounts, get_portfolio, get_equity_positions, get_option_positions, get_equity_orders, and get_option_orders. Do not call any @@ -2764,14 +1726,9 @@ def get_account_snapshot(self) -> AccountSnapshot: of objects containing only symbol, contract_id, quantity, average_cost, market_price, market_value, and unrealized_pnl), recent_equity_orders (array containing only orders from the current and immediately prior Robinhood trading session with symbol, side, status, submitted_at, - filled_at, filled_quantity, average_fill_price, market_date, and every broker-exposed order_type, - time_in_force, limit_price, quantity, notional_usd, and fees_usd field), recent_option_orders (the same - fields plus expiry, strike, option_type, and contract_id), and error (string). Use an empty array -when a position collection is unavailable. Every nonzero position must retain broker-exposed -quantity, average_cost, market_price, market_value, and unrealized_pnl; if any one is unavailable, -leave it absent and explain the omission in error. fees_usd and order economics must likewise remain -absent when the MCP result does not expose them; never infer, zero-fill, or fabricate those fields. -Include equity_order_history_complete and option_order_history_complete booleans; set + filled_at, filled_quantity, average_fill_price, and market_date), recent_option_orders (the same + fields plus expiry, strike, option_type, and contract_id), and error (string). Use 0 or an empty array when a numeric or position value is +unavailable. Include equity_order_history_complete and option_order_history_complete booleans; set each true only when the corresponding order-history tool completed successfully and the returned array covers the current and immediately prior Robinhood trading session. Include order_id in every order object. Set account_type to the selected account's brokerage_account_type, never its @@ -2822,10 +1779,9 @@ def get_account_snapshot(self) -> AccountSnapshot: raise RobinhoodGatewayError( "Robinhood Agentic authenticated field must be boolean" ) - if ( - not isinstance(payload.get("account_type"), str) - or not str(payload.get("account_type", "")).strip() - ): + if not isinstance(payload.get("account_type"), str) or not str( + payload.get("account_type", "") + ).strip(): raise RobinhoodGatewayError( "Robinhood Agentic account_type must be a nonempty string" ) @@ -2836,10 +1792,9 @@ def get_account_snapshot(self) -> AccountSnapshot: raise RobinhoodGatewayError( "Robinhood normalized account_type is not bound to get_accounts" ) - if ( - type(payload.get("equity_order_history_complete")) is not bool - or type(payload.get("option_order_history_complete")) is not bool - ): + if type(payload.get("equity_order_history_complete")) is not bool or type( + payload.get("option_order_history_complete") + ) is not bool: raise RobinhoodGatewayError( "Robinhood Agentic order-history completeness fields must be boolean" ) @@ -2912,7 +1867,8 @@ def get_account_snapshot(self) -> AccountSnapshot: ) if ( payload["equity_order_history_complete"] is not equity_history_complete - or payload["option_order_history_complete"] is not option_history_complete + or payload["option_order_history_complete"] + is not option_history_complete ): raise RobinhoodGatewayError( "Robinhood normalized history completeness is not bound to MCP results" @@ -2975,32 +1931,30 @@ def get_account_snapshot(self) -> AccountSnapshot: "average_fill_price", "market_date", ) - raw_equity_orders = tuple( - _canonical_raw_broker_item(item, "equity_order") - for item in _result_items( - equity_order_call, - frozenset({"equity_orders", "orders", "results"}), - "equity_order", - ) - ) - raw_option_orders = tuple( - _canonical_raw_broker_item(item, "option_order") - for item in _result_items( - option_order_call, - frozenset({"option_orders", "orders", "results"}), - "option_order", - ) - ) _bind_normalized_items( validated_equity_orders, - raw_equity_orders, + tuple( + _canonical_raw_broker_item(item, "equity_order") + for item in _result_items( + equity_order_call, + frozenset({"equity_orders", "orders", "results"}), + "equity_order", + ) + ), ("order_id",), equity_order_fields, "equity_order", ) _bind_normalized_items( validated_option_orders, - raw_option_orders, + tuple( + _canonical_raw_broker_item(item, "option_order") + for item in _result_items( + option_order_call, + frozenset({"option_orders", "orders", "results"}), + "option_order", + ) + ), ("order_id",), ( *equity_order_fields, @@ -3011,28 +1965,6 @@ def get_account_snapshot(self) -> AccountSnapshot: ), "option_order", ) - optional_order_economics = ( - "order_type", - "time_in_force", - "limit_price", - "quantity", - "notional_usd", - "fees_usd", - ) - _bind_optional_item_fields( - validated_equity_orders, - raw_equity_orders, - "order_id", - optional_order_economics, - "equity_order", - ) - _bind_optional_item_fields( - validated_option_orders, - raw_option_orders, - "order_id", - optional_order_economics, - "option_order", - ) return AccountSnapshot( authenticated=payload.get("authenticated") is True, account_type=str(payload.get("account_type", "")), @@ -3047,9 +1979,6 @@ def get_account_snapshot(self) -> AccountSnapshot: option_order_history_complete=payload.get("option_order_history_complete") is True, error=str(payload.get("error", "")), - account_hash=hashlib.sha256( - account_attestation.account_id.encode("utf-8") - ).hexdigest(), ) def get_equity_execution_snapshot( @@ -3066,8 +1995,6 @@ def get_equity_execution_snapshot( raise RobinhoodGatewayError( "Equity execution snapshot requires valid execution and analysis symbols" ) - if getattr(self, "_deterministic_mcp_call", None) is not None: - return self._get_equity_snapshot_deterministically(intent) prompt = f"""Use only Robinhood Trading MCP tools get_equity_quotes and get_equity_tradability. Do not call account, portfolio, order-history, review, place, replace, cancel, watchlist, scan, shell, browser, search, or any other tool. Request current quotes and @@ -3089,84 +2016,8 @@ def get_equity_execution_snapshot( _attest_equity_execution_snapshot(envelope.calls, intent, snapshot) return snapshot - def cancel_order( - self, - instrument: str, - order_id: str, - ) -> GatewayCancelResult: - """Cancel one exact open order through the official Agentic MCP only.""" - normalized_order_id = order_id.strip() - if ( - not normalized_order_id - or len(normalized_order_id) > 256 - or any(character.isspace() for character in normalized_order_id) - ): - raise RobinhoodGatewayError("Cancellation requires a safe broker order_id") - if instrument == "equity": - cancel_tool = "cancel_equity_order" - elif instrument == "option": - cancel_tool = "cancel_option_order" - else: - raise RobinhoodGatewayError( - f"Unsupported cancellation instrument: {instrument!r}" - ) - if self._deterministic_mcp_call is None: - raise RobinhoodGatewayError( - "Live cancellation requires deterministic parent-side MCP" - ) - if instrument != "equity": - raise RobinhoodGatewayError( - "Deterministic live cancellation is currently equity-only" - ) - try: - call = self._call_deterministic_mcp( - cancel_tool, - {"order_id": normalized_order_id}, - self._write_timeout_seconds, - ) - result = _mcp_result_value(call) - observed_order_id = _single_string_field( - result, - ORDER_ID_FIELDS, - "order_id", - ) - if observed_order_id != normalized_order_id: - raise RobinhoodGatewayError( - "Deterministic Robinhood cancellation result changed " - "the exact order ID" - ) - status = _single_string_field( - result, - ORDER_STATUS_FIELDS, - "status", - ).lower() - if status not in RECOGNIZED_ORDER_STATUSES | { - "canceling", - "cancelling", - }: - raise RobinhoodGatewayError( - "Deterministic Robinhood cancellation status is unknown" - ) - return GatewayCancelResult( - cancel_requested=True, - order_id=normalized_order_id, - status=status, - warnings=_direct_result_warnings(result), - error="", - observed_tools=(cancel_tool,), - ) - except RobinhoodGatewayError as error: - raise RobinhoodCancellationAmbiguousError( - "Deterministic Robinhood cancellation has an unknown " - "broker outcome; poll order history before another attempt" - ) from error - def place_order( - self, - intent: LiveExecutionIntent, - paper_twin_record_hash: str, - *, - pre_submit_guard: Callable[[], datetime] | None = None, + self, intent: LiveExecutionIntent, paper_twin_record_hash: str ) -> GatewayOrderResult: if not SYMBOL_PATTERN.fullmatch(intent.symbol): raise RobinhoodGatewayError(f"Invalid order symbol: {intent.symbol!r}") @@ -3188,24 +2039,89 @@ def place_order( raise RobinhoodGatewayError( "Long-equity entries require a validated snapshot and positive limit_price" ) - if intent.instrument not in {"equity", "option"}: + if intent.instrument == "equity": + review_tool = "review_equity_order" + place_tool = "place_equity_order" + elif intent.instrument == "option": + review_tool = "review_option_order" + place_tool = "place_option_order" + else: raise RobinhoodGatewayError( f"Unsupported instrument: {intent.instrument!r}" ) - if intent.instrument != "equity": - raise RobinhoodGatewayError( - "Deterministic live execution is currently equity-only" + exact_intent = json.dumps(intent.to_dict(), sort_keys=True, ensure_ascii=False) + prompt = f"""Standing owner authorization applies to this exact DOPEY Live Branch order. +Use only Robinhood Trading MCP tools get_accounts, get_portfolio, get_equity_quotes, +get_equity_tradability, get_equity_orders, get_option_orders, {review_tool}, and {place_tool}. + Select only the dedicated Agentic trading account. The get_accounts result must expose exactly + one account explicitly labeled Agentic. Pass that exact account ID in every account, history, + review, and placement tool call. Before review, inspect today's equity and option +order history and stop if this exact order would +buy after a same-session sale or sell after a same-session purchase of the same stock or exact +option contract. If and only if the no-day-trade history check passes, the review succeeds, the + account has sufficient buying power/position, the instrument is + tradable, the order remains long-only or sell-to-close, and there is no broker pre-trade blocker, + place exactly the reviewed order without changing symbol, side, amount, contract, order type, or + time-in-force. For every equity buy, request fresh quotes for both symbol and analysis_symbol, + require the execution ask to be at or below the exact limit_price, and place only the exact limit + order. If the broker cannot express the exact amount as a limit order, return + blocked_exact_limit_order_unsupported without placing. When daily_reset is true, treat + analysis_symbol as the thesis authority and symbol only as the execution vehicle. Stop unless a + bullish analysis price is at or above analysis_reference_price, or a bearish analysis price is at + or below analysis_reference_price, and stop if the analysis invalidation or target has already + been crossed. Never infer the thesis from the leveraged ETF chart, never submit a direct short + sale, and never average down. Do not place any additional order. Do not reveal account + identifiers or credentials. +DOPEY already staged mandatory paper twin {paper_twin_record_hash}. +Exact intent JSON: {exact_intent} + Return only JSON with order_history_complete (boolean), review_succeeded (boolean), placed + (boolean), order_id (string), status (string), filled_quantity (number), average_fill_price + (number), submitted_at (string), filled_at (string), warnings (array of strings), and error (string). +order_history_complete must be true before review or placement.""" + required_tools = ( + "get_accounts", + "get_portfolio", + "get_equity_quotes", + "get_equity_tradability", + "get_equity_orders", + "get_option_orders", + ) + try: + envelope = self._run( + f"place_{intent.instrument}_order", + prompt, + required_tools, + (*required_tools, review_tool, place_tool), + 1, ) - if pre_submit_guard is None: - raise RobinhoodGatewayError( - "Deterministic placement requires the final signed-authority guard" + except RobinhoodGatewayError as error: + raise RobinhoodPlacementAmbiguousError( + "Robinhood placement attempt has unknown broker outcome and must not be retried: " + f"instrument={intent.instrument}, symbol={intent.symbol}, intent_id={intent.intent_id}, " + f"error={error}" + ) from error + account = _attest_account_scope( + envelope.calls, + ("get_portfolio", "get_equity_orders", "get_option_orders"), + ) + equity_history_complete, option_history_complete = ( + _attest_order_history_results(envelope.calls) + ) + if type(envelope.payload.get("order_history_complete")) is not bool: + raise RobinhoodPlacementAmbiguousError( + "Robinhood normalized order-history completeness is not boolean" ) - self._validate_pre_submit_guard(pre_submit_guard) - if getattr(self, "_deterministic_mcp_call", None) is None: - raise RobinhoodGatewayError( - "Live placement requires deterministic parent-side MCP" + if envelope.payload["order_history_complete"] is not ( + equity_history_complete and option_history_complete + ): + raise RobinhoodPlacementAmbiguousError( + "Robinhood normalized order-history completeness is not bound to MCP results" ) - return self._place_equity_order_deterministically( + return _gateway_order_result( + envelope.payload, + review_tool, + place_tool, intent, - pre_submit_guard, + envelope.calls, + account.account_id, ) diff --git a/dopey_live/robinhood_mcp_direct.py b/dopey_live/robinhood_mcp_direct.py deleted file mode 100644 index ec09fd2..0000000 --- a/dopey_live/robinhood_mcp_direct.py +++ /dev/null @@ -1,512 +0,0 @@ -"""Deterministic, model-inaccessible Robinhood MCP write transport. - -The local agent never receives these tools. This client opens one bounded -Streamable HTTP MCP session, verifies the selected tool schema, makes exactly -one typed call, and closes the session. It has no retry path. -""" - -from __future__ import annotations - -import hashlib -import json -from dataclasses import dataclass -from typing import Any, Callable, Final - -import requests - -from .control import OFFICIAL_ROBINHOOD_MCP -from .robinhood_gateway import ( - READ_ONLY_ROBINHOOD_TOOLS, - RobinhoodGatewayError, - _agentic_account, - _direct_call_evidence, -) - - -MCP_PROTOCOL_VERSION: Final[str] = "2025-06-18" -MAXIMUM_MCP_RESPONSE_BYTES: Final[int] = 2_097_152 -EXPECTED_TOOL_PROPERTIES: Final[dict[str, frozenset[str]]] = { - "review_equity_order": frozenset( - { - "side", - "symbol", - "quantity", - "order_type", - "limit_price", - "time_in_force", - } - ), - "place_equity_order": frozenset( - { - "side", - "amount", - "symbol", - "quantity", - "order_type", - "limit_price", - "time_in_force", - } - ), - "cancel_equity_order": frozenset({"order_id"}), -} -EXPECTED_REQUIRED_PROPERTIES: Final[dict[str, frozenset[str]]] = { - "review_equity_order": frozenset({"side", "symbol"}), - "place_equity_order": frozenset({"side", "symbol", "order_type"}), - "cancel_equity_order": frozenset({"order_id"}), -} -READ_ARGUMENT_CONTRACTS: Final[dict[str, dict[str, str]]] = { - "get_accounts": {}, - "get_portfolio": {"account_number": "string"}, - "get_equity_positions": {"account_number": "string"}, - "get_option_positions": {"account_number": "string"}, - "get_equity_orders": { - "account_number": "string", - "limit": "integer", - }, - "get_option_orders": { - "account_number": "string", - "limit": "integer", - }, - "get_equity_quotes": {"symbols": "string_array"}, - "get_equity_tradability": {"symbols": "string_array"}, -} - - -def _strict_json(raw: bytes, label: str) -> dict[str, Any]: - def reject_duplicate(pairs: list[tuple[str, Any]]) -> dict[str, Any]: - result: dict[str, Any] = {} - for key, value in pairs: - if key in result: - raise RobinhoodGatewayError(f"{label} contains a duplicate JSON key") - result[key] = value - return result - - def reject_constant(value: str) -> None: - raise RobinhoodGatewayError(f"{label} contains a non-finite number: {value}") - - try: - payload = json.loads( - raw, - object_pairs_hook=reject_duplicate, - parse_constant=reject_constant, - ) - except (UnicodeDecodeError, json.JSONDecodeError) as error: - raise RobinhoodGatewayError(f"{label} is not valid UTF-8 JSON") from error - if not isinstance(payload, dict): - raise RobinhoodGatewayError(f"{label} must be a JSON object") - return payload - - -def _json_rpc_from_response( - response: requests.Response, - request_id: int, -) -> dict[str, Any]: - body = bytes(response.content) - if not 1 <= len(body) <= MAXIMUM_MCP_RESPONSE_BYTES: - raise RobinhoodGatewayError("Robinhood MCP response size is invalid") - content_type = response.headers.get("Content-Type", "").lower() - candidates: list[dict[str, Any]] = [] - if "text/event-stream" in content_type: - for line in body.splitlines(): - if not line.startswith(b"data:"): - continue - raw_event = line.removeprefix(b"data:").strip() - if raw_event: - candidates.append(_strict_json(raw_event, "Robinhood MCP SSE event")) - elif "application/json" in content_type: - candidates.append(_strict_json(body, "Robinhood MCP response")) - else: - raise RobinhoodGatewayError( - "Robinhood MCP response Content-Type is unsupported" - ) - matches = [ - item - for item in candidates - if item.get("jsonrpc") == "2.0" and item.get("id") == request_id - ] - if len(matches) != 1: - raise RobinhoodGatewayError( - "Robinhood MCP response does not bind one request ID" - ) - message = matches[0] - if "error" in message: - raise RobinhoodGatewayError("Robinhood MCP returned a JSON-RPC error") - result = message.get("result") - if not isinstance(result, dict): - raise RobinhoodGatewayError("Robinhood MCP result must be an object") - return result - - -def _verify_tool_schema(tool: str, tools_result: dict[str, Any]) -> None: - tools = tools_result.get("tools") - if not isinstance(tools, list): - raise RobinhoodGatewayError("Robinhood MCP tools/list result is invalid") - matches = [ - item for item in tools if isinstance(item, dict) and item.get("name") == tool - ] - if len(matches) != 1: - raise RobinhoodGatewayError( - "Robinhood MCP selected tool is absent or ambiguous" - ) - schema = matches[0].get("inputSchema") - if not isinstance(schema, dict) or schema.get("type") != "object": - raise RobinhoodGatewayError( - "Robinhood MCP selected tool input schema is invalid" - ) - properties = schema.get("properties") - required = schema.get("required", []) - if ( - not isinstance(properties, dict) - or frozenset(properties) != EXPECTED_TOOL_PROPERTIES[tool] - or not isinstance(required, list) - or not all(isinstance(item, str) for item in required) - or frozenset(required) != EXPECTED_REQUIRED_PROPERTIES[tool] - ): - raise RobinhoodGatewayError( - "Robinhood MCP selected tool schema drifted from the sealed contract" - ) - - -def _verify_read_tool_schema( - tool: str, - arguments: dict[str, Any], - tools_result: dict[str, Any], -) -> None: - tools = tools_result.get("tools") - if not isinstance(tools, list): - raise RobinhoodGatewayError("Robinhood MCP tools/list result is invalid") - matches = [ - item for item in tools if isinstance(item, dict) and item.get("name") == tool - ] - if len(matches) != 1: - raise RobinhoodGatewayError( - "Robinhood MCP selected read tool is absent or ambiguous" - ) - schema = matches[0].get("inputSchema") - if not isinstance(schema, dict) or schema.get("type") != "object": - raise RobinhoodGatewayError( - "Robinhood MCP selected read tool input schema is invalid" - ) - properties = schema.get("properties", {}) - required = schema.get("required", []) - expected_contract = READ_ARGUMENT_CONTRACTS.get(tool) - if expected_contract is None or frozenset(arguments) != frozenset( - expected_contract - ): - raise RobinhoodGatewayError( - "Robinhood MCP read arguments differ from the sealed contract" - ) - if ( - not isinstance(properties, dict) - or not isinstance(required, list) - or not all(isinstance(item, str) for item in required) - or not frozenset(arguments).issubset(properties) - or not frozenset(required).issubset(arguments) - ): - raise RobinhoodGatewayError( - "Robinhood MCP read arguments do not satisfy the live tool schema" - ) - for field, expected_type in expected_contract.items(): - property_schema = properties.get(field) - if not isinstance(property_schema, dict): - raise RobinhoodGatewayError("Robinhood MCP read property schema is invalid") - if expected_type == "string": - valid_schema = property_schema.get("type") == "string" - valid_value = ( - isinstance(arguments[field], str) - and bool(arguments[field].strip()) - and len(arguments[field]) <= 512 - ) - elif expected_type == "integer": - valid_schema = property_schema.get("type") == "integer" - valid_value = ( - not isinstance(arguments[field], bool) - and isinstance(arguments[field], int) - and arguments[field] == 1_000 - ) - else: - items = property_schema.get("items") - valid_schema = ( - property_schema.get("type") == "array" - and isinstance(items, dict) - and items.get("type") == "string" - ) - value = arguments[field] - valid_value = ( - isinstance(value, list) - and 1 <= len(value) <= 2 - and all(isinstance(item, str) and bool(item.strip()) for item in value) - and value == sorted(set(value)) - ) - if not valid_schema or not valid_value: - raise RobinhoodGatewayError( - "Robinhood MCP read property type drifted from the sealed contract" - ) - - -def _verify_arguments(tool: str, arguments: dict[str, Any]) -> None: - keys = frozenset(arguments) - if not EXPECTED_REQUIRED_PROPERTIES[tool].issubset(keys): - raise RobinhoodGatewayError( - "Deterministic Robinhood MCP call lacks required arguments" - ) - if not keys.issubset(EXPECTED_TOOL_PROPERTIES[tool]): - raise RobinhoodGatewayError( - "Deterministic Robinhood MCP call contains unexpected arguments" - ) - if tool in {"review_equity_order", "place_equity_order"}: - if "amount" in keys or "quantity" not in keys: - raise RobinhoodGatewayError( - "Deterministic equity orders require exact quantity mode" - ) - - -@dataclass -class DeterministicRobinhoodMcpClient: - """One-shot official MCP caller unavailable to the generative subprocess.""" - - bearer_token: str - expected_account_hash: str - session: requests.Session | None = None - - def __post_init__(self) -> None: - if ( - not isinstance(self.bearer_token, str) - or not 16 <= len(self.bearer_token) <= 16_384 - or self.bearer_token != self.bearer_token.strip() - or any(character in self.bearer_token for character in "\r\n\0") - ): - raise ValueError("Robinhood MCP bearer profile is invalid") - if ( - not isinstance(self.expected_account_hash, str) - or len(self.expected_account_hash) != 64 - or any( - character not in "0123456789abcdef" - for character in self.expected_account_hash - ) - ): - raise ValueError("Robinhood MCP expected account hash is invalid") - if self.session is None: - self.session = requests.Session() - self.session.trust_env = False - - def _request( - self, - method: str, - payload: dict[str, Any] | None, - timeout_seconds: int, - *, - request_id: int | None, - session_id: str | None, - ) -> tuple[dict[str, Any] | None, str | None]: - if self.session is None: - raise RobinhoodGatewayError("Robinhood MCP HTTP session is unavailable") - headers = { - "Accept": "application/json, text/event-stream", - "Authorization": f"Bearer {self.bearer_token}", - "Cache-Control": "no-store", - "Content-Type": "application/json", - "MCP-Protocol-Version": MCP_PROTOCOL_VERSION, - } - if session_id is not None: - headers["Mcp-Session-Id"] = session_id - try: - response = self.session.request( - method, - OFFICIAL_ROBINHOOD_MCP, - headers=headers, - json=payload, - timeout=timeout_seconds, - allow_redirects=False, - ) - except requests.RequestException as error: - raise RobinhoodGatewayError( - "Robinhood MCP deterministic transport failed" - ) from error - if response.status_code not in {200, 202}: - raise RobinhoodGatewayError( - "Robinhood MCP deterministic transport returned a non-success status" - ) - returned_session = response.headers.get("Mcp-Session-Id") - if returned_session is not None and ( - not returned_session - or len(returned_session) > 512 - or any(character in returned_session for character in "\r\n\0") - ): - raise RobinhoodGatewayError("Robinhood MCP session ID is invalid") - if request_id is None: - return None, returned_session - return _json_rpc_from_response(response, request_id), returned_session - - def _call_initialized( - self, - tool: str, - arguments: dict[str, Any], - timeout_seconds: int, - session_id: str | None, - pre_call_guard: Callable[[], None] | None, - ) -> dict[str, Any]: - self._request( - "POST", - { - "jsonrpc": "2.0", - "method": "notifications/initialized", - }, - timeout_seconds, - request_id=None, - session_id=session_id, - ) - tools_result, _ = self._request( - "POST", - { - "jsonrpc": "2.0", - "id": 2, - "method": "tools/list", - "params": {}, - }, - timeout_seconds, - request_id=2, - session_id=session_id, - ) - if tools_result is None: - raise RobinhoodGatewayError("Robinhood MCP tools/list result is absent") - if tool in EXPECTED_TOOL_PROPERTIES: - _verify_tool_schema(tool, tools_result) - else: - _verify_read_tool_schema(tool, arguments, tools_result) - next_request_id = 3 - if tool in EXPECTED_TOOL_PROPERTIES: - _verify_read_tool_schema("get_accounts", {}, tools_result) - account_result, _ = self._request( - "POST", - { - "jsonrpc": "2.0", - "id": next_request_id, - "method": "tools/call", - "params": { - "name": "get_accounts", - "arguments": {}, - }, - }, - timeout_seconds, - request_id=next_request_id, - session_id=session_id, - ) - if account_result is None: - raise RobinhoodGatewayError("Robinhood MCP account proof is absent") - account = _agentic_account( - ( - _direct_call_evidence( - "get_accounts", - {}, - account_result, - ), - ) - ) - if ( - hashlib.sha256(account.account_id.encode("utf-8")).hexdigest() - != self.expected_account_hash - ): - raise RobinhoodGatewayError( - "Robinhood MCP session is not bound to the pinned Agentic account" - ) - next_request_id += 1 - if tool == "place_equity_order": - if pre_call_guard is None: - raise RobinhoodGatewayError( - "Robinhood placement requires an immediate authority guard" - ) - pre_call_guard() - elif pre_call_guard is not None: - raise RobinhoodGatewayError( - "Immediate authority guard is only valid for placement" - ) - call_result, _ = self._request( - "POST", - { - "jsonrpc": "2.0", - "id": next_request_id, - "method": "tools/call", - "params": { - "name": tool, - "arguments": dict(arguments), - }, - }, - timeout_seconds, - request_id=next_request_id, - session_id=session_id, - ) - if call_result is None: - raise RobinhoodGatewayError("Robinhood MCP tools/call result is absent") - return call_result - - def __call__( - self, - tool: str, - arguments: dict[str, Any], - timeout_seconds: int, - pre_call_guard: Callable[[], None] | None = None, - ) -> dict[str, Any]: - if tool not in EXPECTED_TOOL_PROPERTIES and tool not in ( - READ_ONLY_ROBINHOOD_TOOLS - ): - raise RobinhoodGatewayError( - "Robinhood deterministic tool is outside the sealed allowlist" - ) - if ( - isinstance(timeout_seconds, bool) - or not isinstance(timeout_seconds, int) - or not 1 <= timeout_seconds <= 60 - ): - raise ValueError("Robinhood deterministic timeout is invalid") - if tool in EXPECTED_TOOL_PROPERTIES: - _verify_arguments(tool, arguments) - elif pre_call_guard is not None: - raise RobinhoodGatewayError( - "Immediate authority guard is only valid for placement" - ) - initialize, session_id = self._request( - "POST", - { - "jsonrpc": "2.0", - "id": 1, - "method": "initialize", - "params": { - "protocolVersion": MCP_PROTOCOL_VERSION, - "capabilities": {}, - "clientInfo": { - "name": "DumbMoneyDopeyRobinhood", - "version": "1", - }, - }, - }, - timeout_seconds, - request_id=1, - session_id=None, - ) - try: - if initialize is None or initialize.get("protocolVersion") != ( - MCP_PROTOCOL_VERSION - ): - raise RobinhoodGatewayError( - "Robinhood MCP protocol version is not the sealed version" - ) - return self._call_initialized( - tool, - arguments, - timeout_seconds, - session_id, - pre_call_guard, - ) - finally: - if session_id is not None: - try: - self._request( - "DELETE", - None, - timeout_seconds, - request_id=None, - session_id=session_id, - ) - except RobinhoodGatewayError: - pass diff --git a/dopey_live/service.py b/dopey_live/service.py index f77ee83..356216b 100644 --- a/dopey_live/service.py +++ b/dopey_live/service.py @@ -9,30 +9,14 @@ import subprocess from datetime import date, datetime, timedelta, timezone from pathlib import Path -from typing import Any, Callable +from typing import Any from zoneinfo import ZoneInfo import exchange_calendars as xcals -from .control import LiveBranchControl, load_live_branch_control -from .dumbmoney_command_feed import DumbMoneyCommandDispatch -from .dumbmoney_authority import ( - DumbMoneyAuthorityContext, - DumbMoneyAuthorityError, - VerifiedCapitalAuthority, - authorize_intent, - authorized_settled_cash_usd, -) -from .dumbmoney_promotion import ( - PromotionAuthorityResolver, - VerifiedPromotionBinding, -) +from .control import load_live_branch_control from .ledger import ( - LiveBranchLedgerError, - activate_dumbmoney_fence, append_record, - assert_current_dumbmoney_fence, - artifact_root, ledger_path, paper_twin_path, read_ledger, @@ -47,7 +31,6 @@ from .robinhood_gateway import ( RobinhoodAgenticGateway, RobinhoodAuthRequiredError, - RobinhoodCancellationAmbiguousError, RobinhoodConsentRequiredError, RobinhoodGatewayError, RobinhoodPlacementAmbiguousError, @@ -60,7 +43,6 @@ build_entry_intents, prepare_equity_entry_for_execution, research_maturity_blockers, - validate_option_entry_for_execution, ) @@ -73,8 +55,6 @@ ) RECOGNIZED_ORDER_STATUSES = ACTIVE_ORDER_STATUSES | TERMINAL_ORDER_STATUSES MAX_ORDER_INTENTS_PER_CYCLE = 1 -DUMBMONEY_KILL_FILENAME = "DUMBMONEY_KILL.v1" -DUMBMONEY_HISTORY_POLL_LIMIT = 3 def _regular_market_state(now_utc: datetime) -> tuple[bool, str]: @@ -189,13 +169,7 @@ def _paper_twin_execution_payload( quote = snapshot.execution_quote marketable = intent.limit_price >= quote.ask_price filled_quantity = ( - ( - intent.quantity - if intent.quantity > 0.0 - else round(intent.notional_usd / quote.ask_price, 8) - ) - if marketable - else 0.0 + round(intent.notional_usd / quote.ask_price, 8) if marketable else 0.0 ) if marketable and filled_quantity <= 0.0: raise ValueError( @@ -206,9 +180,7 @@ def _paper_twin_execution_payload( return { **base, "paper_twin_execution_status": "independent_quote_observation_complete", - "paper_fill_status": "simulated_filled" - if marketable - else "simulated_not_filled", + "paper_fill_status": "simulated_filled" if marketable else "simulated_not_filled", "paper_filled_quantity": filled_quantity, "paper_average_fill_price": quote.ask_price if marketable else None, "market_observation_source": snapshot.source, @@ -411,10 +383,7 @@ def _open_live_positions( # _settle_owner_liquidations. Subtraction order vs system sells does not # matter: both only ever subtract nonnegative quantities. for record in records: - if ( - str(record.get("event_type", "")) - != "live_position_owner_liquidation_observed" - ): + if str(record.get("event_type", "")) != "live_position_owner_liquidation_observed": continue payload = record.get("payload") if not isinstance(payload, dict): @@ -928,7 +897,9 @@ def _order_history_blocker( evidence_order_ids = payload.get("evidence_broker_order_ids", []) if isinstance(evidence_order_ids, list): local_order_ids.update( - str(value) for value in evidence_order_ids if str(value).strip() + str(value) + for value in evidence_order_ids + if str(value).strip() ) continue placed_records.append(payload) @@ -995,570 +966,15 @@ def _order_history_blocker( return "" -def dumbmoney_kill_path(repository_root: Path) -> Path: - return artifact_root(repository_root) / DUMBMONEY_KILL_FILENAME - - -def _active_attributed_orders( - repository_root: Path, - account: AccountSnapshot, -) -> tuple[tuple[str, str], ...]: - instruments_by_order_id: dict[str, str] = {} - for record in read_ledger(ledger_path(repository_root)): - if record.get("event_type") not in { - "live_order_result", - "live_order_submission_ambiguous", - }: - continue - payload = record.get("payload") - if not isinstance(payload, dict): - continue - is_definite_placement = ( - record.get("event_type") == "live_order_result" - and payload.get("placed") is True - ) - is_attributed_ambiguous_dispatch = ( - record.get("event_type") == "live_order_submission_ambiguous" - and payload.get("in_flight_authority_revoked") is True - ) - if not (is_definite_placement or is_attributed_ambiguous_dispatch): - continue - order_id = str(payload.get("order_id", "")).strip() - instrument = str(payload.get("instrument", "")).strip() - if order_id and instrument in {"equity", "option"}: - instruments_by_order_id[order_id] = instrument - active: list[tuple[str, str]] = [] - for instrument, orders in ( - ("equity", account.recent_equity_orders), - ("option", account.recent_option_orders), - ): - for order in orders: - order_id = str(order.get("order_id", "")).strip() - status = str(order.get("status", "")).strip().lower() - if ( - order_id - and status in ACTIVE_ORDER_STATUSES - and instruments_by_order_id.get(order_id) == instrument - ): - active.append((instrument, order_id)) - return tuple(sorted(set(active))) - - -def _run_cancel_only_degraded( - repository_root: Path, - gateway: RobinhoodAgenticGateway, - account: AccountSnapshot, - now_utc: datetime, - reason: str, - request_cancellation: bool, - *, - effective_kill_active: bool | None = None, -) -> dict[str, Any]: - attempts: list[dict[str, Any]] = [] - current_account = account - targets = _active_attributed_orders(repository_root, current_account) - if request_cancellation: - for instrument, order_id in targets: - append_record( - ledger_path(repository_root), - "dumbmoney_cancel_started", - { - "instrument": instrument, - "order_id": order_id, - "reason": reason, - "new_exposure_allowed": False, - }, - ) - outcome: dict[str, Any] - try: - cancellation = gateway.cancel_order(instrument, order_id) - except RobinhoodCancellationAmbiguousError as error: - outcome = { - "instrument": instrument, - "order_id": order_id, - "cancel_requested": None, - "status": "ambiguous_cancel_history_poll_required", - "error": str(error), - } - append_record( - ledger_path(repository_root), - "dumbmoney_cancel_ambiguous", - outcome, - ) - except RobinhoodGatewayError as error: - outcome = { - "instrument": instrument, - "order_id": order_id, - "cancel_requested": False, - "status": "cancel_gateway_blocked", - "error": str(error), - } - append_record( - ledger_path(repository_root), - "dumbmoney_cancel_blocked", - outcome, - ) - else: - outcome = { - "instrument": instrument, - "order_id": order_id, - **cancellation.to_dict(), - } - append_record( - ledger_path(repository_root), - "dumbmoney_cancel_result", - outcome, - ) - attempts.append(outcome) - poll_count = 0 - poll_errors: list[str] = [] - for _ in range(DUMBMONEY_HISTORY_POLL_LIMIT if request_cancellation else 1): - poll_count += 1 - try: - current_account = gateway.get_account_snapshot() - except RobinhoodGatewayError as error: - poll_errors.append(f"{type(error).__name__}: {error}") - continue - _reconcile_live_order_statuses(repository_root, current_account) - if not _active_attributed_orders(repository_root, current_account): - break - unresolved = _active_attributed_orders(repository_root, current_account) - return { - "cycle_status": "cancel_only_degraded", - "authority_state": "CANCEL_ONLY_DEGRADED", - "cancel_only_reason": reason, - "new_exposure_allowed": False, - "kill_active": ( - dumbmoney_kill_path(repository_root).is_file() - if effective_kill_active is None - else ( - effective_kill_active or dumbmoney_kill_path(repository_root).is_file() - ) - ), - "cancel_requested": request_cancellation, - "cancel_target_count": len(targets), - "cancel_attempts": attempts, - "history_poll_count": poll_count, - "history_poll_errors": poll_errors, - "unresolved_attributed_orders": [ - {"instrument": instrument, "order_id": order_id} - for instrument, order_id in unresolved - ], - "account": current_account.to_dict(), - "orders": [], - } - - -def _required_exact_number( - order: dict[str, Any], - fields: tuple[str, ...], - expected: float, -) -> bool: - values = [order[field] for field in fields if field in order] - if not values: - return False - try: - return len({float(value) for value in values}) == 1 and math.isclose( - float(values[0]), - expected, - rel_tol=0.0, - abs_tol=1e-9, - ) - except (TypeError, ValueError): - return False - - -def _exact_ambiguous_history_match( - order: dict[str, Any], - intent: LiveExecutionIntent, - market_date: str, - attempted_at: datetime, -) -> bool: - if ( - str(order.get("symbol", "")).upper() != intent.symbol - or str(order.get("side", "")).lower() != intent.side - or str(order.get("market_date", "")) != market_date - ): - return False - try: - submitted_at = datetime.fromisoformat( - str(order.get("submitted_at", "")).replace("Z", "+00:00") - ) - except ValueError: - return False - if ( - submitted_at.tzinfo is None - or abs((submitted_at - attempted_at).total_seconds()) > 300 - ): - return False - if str(order.get("order_type", "")).lower() != intent.order_type: - return False - if str(order.get("time_in_force", "")).lower() != intent.time_in_force: - return False - if intent.order_type != "market" and not _required_exact_number( - order, - ("limit", "limit_price"), - intent.limit_price, - ): - return False - if intent.quantity > 0.0: - if not _required_exact_number( - order, - ("asset_quantity", "quantity", "share_quantity", "shares"), - intent.quantity, - ): - return False - elif not _required_exact_number( - order, - ("amount", "dollar_amount", "notional", "notional_usd"), - intent.notional_usd, - ): - return False - return _order_matches_intent(order, intent) - - -def _witnessed_daily_loss_cents( - repository_root: Path, - account: AccountSnapshot, - market_date: str, -) -> int: - """Derive a conservative daily loss from complete broker history + local lineage. - - A filled order without explicit broker fee evidence, local attribution, or - enough prior long inventory makes the baseline unknowable and therefore - blocks new exposure. - """ - if ( - not market_date - or not account.equity_order_history_complete - or not account.option_order_history_complete - ): - raise DumbMoneyAuthorityError( - "Complete broker order history is required for daily-loss authority" - ) - intents_by_order_id: dict[str, LiveExecutionIntent] = {} - for record in read_ledger(ledger_path(repository_root)): - if record.get("event_type") != "live_order_result": - continue - payload = record.get("payload") - if not isinstance(payload, dict) or payload.get("placed") is not True: - continue - order_id = str(payload.get("order_id", "")).strip() - raw_intent = payload.get("intent") - if not order_id or not isinstance(raw_intent, dict): - raise DumbMoneyAuthorityError( - "Placed local order lacks broker identity or typed intent" - ) - try: - intents_by_order_id[order_id] = live_execution_intent_from_dict(raw_intent) - except (TypeError, ValueError) as error: - raise DumbMoneyAuthorityError( - "Placed local order has an invalid typed intent" - ) from error - broker_orders = [ - *account.recent_equity_orders, - *account.recent_option_orders, - ] - try: - broker_orders.sort( - key=lambda order: ( - datetime.fromisoformat( - str(order.get("submitted_at", "")).replace("Z", "+00:00") - ), - str(order.get("order_id", "")), - ) - ) - except ValueError as error: - raise DumbMoneyAuthorityError( - "Broker order history has an invalid submission timestamp" - ) from error - lots: dict[str, list[list[float]]] = {} - daily_pnl_usd = 0.0 - for order in broker_orders: - status = str(order.get("status", "")).lower() - if status not in {"filled", "partially_filled"}: - continue - order_id = str(order.get("order_id", "")).strip() - intent = intents_by_order_id.get(order_id) - if intent is None: - raise DumbMoneyAuthorityError( - "Filled broker order is not attributed to a typed local intent" - ) - if str( - order.get("side", "") - ).lower() != intent.side or not _order_matches_intent(order, intent): - raise DumbMoneyAuthorityError( - "Broker fill identity conflicts with its typed local intent" - ) - try: - quantity = float(order.get("filled_quantity", 0.0)) - average_price = float(order.get("average_fill_price", 0.0)) - except (TypeError, ValueError) as error: - raise DumbMoneyAuthorityError( - "Broker fill economics are invalid" - ) from error - fee_values = [order[field] for field in ("fees_usd", "fees") if field in order] - if len(fee_values) != 1: - raise DumbMoneyAuthorityError( - "Filled broker order lacks one explicit fee observation" - ) - try: - fee_usd = float(fee_values[0]) - except (TypeError, ValueError) as error: - raise DumbMoneyAuthorityError("Broker fill fee is invalid") from error - if ( - not math.isfinite(quantity) - or not math.isfinite(average_price) - or not math.isfinite(fee_usd) - or quantity <= 0.0 - or average_price <= 0.0 - or fee_usd < 0.0 - ): - raise DumbMoneyAuthorityError( - "Broker fill quantity, price, and fee must be finite and bounded" - ) - key = _position_key(intent) - multiplier = 100.0 if intent.instrument == "option" else 1.0 - if intent.side == "buy": - lots.setdefault(key, []).append([quantity, average_price]) - if str(order.get("market_date", "")) == market_date: - daily_pnl_usd -= fee_usd - continue - remaining = quantity - realized = -fee_usd - position_lots = lots.setdefault(key, []) - while remaining > 1e-9 and position_lots: - lot_quantity, lot_price = position_lots[0] - consumed = min(remaining, lot_quantity) - realized += (average_price - lot_price) * consumed * multiplier - remaining -= consumed - lot_quantity -= consumed - if lot_quantity <= 1e-9: - position_lots.pop(0) - else: - position_lots[0][0] = lot_quantity - if remaining > 1e-9: - raise DumbMoneyAuthorityError( - "Broker sell fill exceeds witnessed long inventory" - ) - if str(order.get("market_date", "")) == market_date: - daily_pnl_usd += realized - if not math.isfinite(daily_pnl_usd): - raise DumbMoneyAuthorityError("Witnessed daily PnL is not finite") - unrealized_loss_usd = 0.0 - for position in (*account.equity_positions, *account.option_positions): - try: - quantity = float(position.get("quantity", 0.0)) - unrealized_pnl = float(position["unrealized_pnl"]) - except (KeyError, TypeError, ValueError) as error: - raise DumbMoneyAuthorityError( - "Nonzero broker position lacks witnessed unrealized PnL" - ) from error - if not math.isfinite(quantity) or not math.isfinite(unrealized_pnl): - raise DumbMoneyAuthorityError( - "Broker position unrealized PnL evidence is not finite" - ) - if quantity != 0.0: - unrealized_loss_usd += max(0.0, -unrealized_pnl) - realized_loss_cents = max(0, math.ceil(-daily_pnl_usd * 100.0 - 1e-9)) - unrealized_loss_cents = math.ceil(unrealized_loss_usd * 100.0 - 1e-9) - return realized_loss_cents + unrealized_loss_cents - - -def _poll_ambiguous_placement_history( - repository_root: Path, - gateway: RobinhoodAgenticGateway, - intent: LiveExecutionIntent, - market_date: str, - attempted_at: datetime, - paper_twin_record_hash: str, -) -> tuple[dict[str, Any] | None, AccountSnapshot | None, int]: - known_order_ids = { - str(record.get("payload", {}).get("order_id", "")) - for record in read_ledger(ledger_path(repository_root)) - if isinstance(record.get("payload"), dict) - and str(record.get("payload", {}).get("order_id", "")) - } - candidates: dict[str, dict[str, Any]] = {} - latest_account: AccountSnapshot | None = None - poll_errors: list[str] = [] - poll_count = 0 - for poll_index in range(1, DUMBMONEY_HISTORY_POLL_LIMIT + 1): - poll_count = poll_index - try: - latest_account = gateway.get_account_snapshot() - except RobinhoodGatewayError as error: - poll_errors.append(f"{type(error).__name__}: {error}") - continue - orders = ( - latest_account.recent_equity_orders - if intent.instrument == "equity" - else latest_account.recent_option_orders - ) - for order in orders: - order_id = str(order.get("order_id", "")).strip() - if ( - order_id - and order_id not in known_order_ids - and _exact_ambiguous_history_match( - order, - intent, - market_date, - attempted_at, - ) - ): - candidates[order_id] = order - append_record( - ledger_path(repository_root), - "dumbmoney_ambiguous_history_poll", - { - "intent_id": intent.intent_id, - "poll_index": poll_index, - "candidate_order_ids": sorted(candidates), - "candidate_count": len(candidates), - "history_complete": ( - latest_account.equity_order_history_complete - if intent.instrument == "equity" - else latest_account.option_order_history_complete - ), - }, - ) - if latest_account is None or len(candidates) != 1: - if poll_errors: - append_record( - ledger_path(repository_root), - "dumbmoney_ambiguous_history_poll_failed", - { - "intent_id": intent.intent_id, - "poll_error_count": len(poll_errors), - "errors": poll_errors, - }, - ) - return None, latest_account, poll_count - order = next(iter(candidates.values())) - history_complete = ( - latest_account.equity_order_history_complete - if intent.instrument == "equity" - else latest_account.option_order_history_complete - ) - if not history_complete: - return None, latest_account, poll_count - payload = { - "intent_id": intent.intent_id, - "instrument": intent.instrument, - "symbol": intent.symbol, - "side": intent.side, - "market_date": market_date, - "paper_twin_record_hash": paper_twin_record_hash, - "intent": intent.to_dict(), - "review_succeeded": True, - "placed": True, - "order_id": str(order["order_id"]), - "status": str(order.get("status", "")).lower(), - "filled_quantity": _filled_quantity( - order, - f"ambiguous_history_order:{order['order_id']}", - ), - "average_fill_price": float(order.get("average_fill_price", 0.0)), - "submitted_at": str(order.get("submitted_at", "")), - "filled_at": str(order.get("filled_at", "")), - "warnings": ["automatically_reconciled_from_exact_complete_broker_history"], - "error": "", - "observed_tools": ["get_accounts", "get_equity_orders", "get_option_orders"], - "automatic_history_reconciled": True, - "placement_retried": False, - } - reconciliation_record = append_record( - ledger_path(repository_root), - "live_order_result", - payload, - ) - return ( - { - **payload, - "automatic_history_record_hash": reconciliation_record["record_hash"], - }, - latest_account, - poll_count, - ) - - def run_live_branch_cycle( repository_root: Path, gateway: RobinhoodAgenticGateway, now_utc: datetime, execute_orders: bool, - *, - dumbmoney_authority: DumbMoneyAuthorityContext | None = None, - dumbmoney_controls: DumbMoneyCommandDispatch | None = None, - dumbmoney_control_refresh: ( - Callable[[datetime], DumbMoneyCommandDispatch] | None - ) = None, - dumbmoney_promotion_resolver: PromotionAuthorityResolver | None = None, - authority_clock: Callable[[], datetime] | None = None, - live_branch_control: LiveBranchControl | None = None, ) -> dict[str, Any]: - control = ( - live_branch_control - if live_branch_control is not None - else load_live_branch_control() - ) - if dumbmoney_authority is not None and dumbmoney_controls is not None: - result = { - **_base_cycle(now_utc), - "dumbmoney_authority_enabled": False, - "dumbmoney_command_feed": dumbmoney_controls.evidence(), - "cycle_status": "blocked_dumbmoney_control_source_conflict", - "orders": [], - } - write_latest_cycle(repository_root, result) - return result - if dumbmoney_control_refresh is not None and dumbmoney_controls is None: - result = { - **_base_cycle(now_utc), - "dumbmoney_authority_enabled": False, - "dumbmoney_command_feed": None, - "cycle_status": "blocked_dumbmoney_control_refresh_without_snapshot", - "orders": [], - } - write_latest_cycle(repository_root, result) - return result - effective_authority = ( - dumbmoney_controls.capital_authority - if dumbmoney_controls is not None - else dumbmoney_authority - ) - dumbmoney_configured = ( - dumbmoney_controls is not None or dumbmoney_authority is not None - ) - feed_kill_active = ( - dumbmoney_controls.kill_active if dumbmoney_controls is not None else False - ) - local_kill_active = ( - dumbmoney_configured and dumbmoney_kill_path(repository_root).is_file() - ) - effective_kill_active = feed_kill_active or local_kill_active - feed_blocks_new_risk = ( - dumbmoney_controls is not None and not dumbmoney_controls.allow_new_risk - ) - feed_refresh_missing = ( - execute_orders - and dumbmoney_controls is not None - and dumbmoney_controls.allow_new_risk - and dumbmoney_control_refresh is None - ) - feed_blocks_new_risk = feed_blocks_new_risk or feed_refresh_missing - base = { - **_base_cycle(now_utc), - "dumbmoney_authority_enabled": effective_authority is not None, - "dumbmoney_command_feed": ( - dumbmoney_controls.evidence() if dumbmoney_controls is not None else None - ), - } - kill_requested = dumbmoney_configured and ( - effective_kill_active or feed_blocks_new_risk - ) - if not control.live_branch_enabled and not kill_requested: + control = load_live_branch_control() + base = _base_cycle(now_utc) + if not control.live_branch_enabled: result = {**base, "cycle_status": "disabled_by_owner_control", "orders": []} write_latest_cycle(repository_root, result) return result @@ -1621,68 +1037,6 @@ def run_live_branch_cycle( } write_latest_cycle(repository_root, result) return result - if dumbmoney_controls is not None and effective_kill_active: - degraded = _run_cancel_only_degraded( - repository_root, - gateway, - account, - now_utc, - ( - dumbmoney_controls.fail_closed_reason - or "dumbmoney_command_feed_kill_active" - ), - True, - effective_kill_active=True, - ) - result = {**base, **degraded} - write_latest_cycle(repository_root, result) - return result - if dumbmoney_controls is not None and feed_blocks_new_risk: - degraded = _run_cancel_only_degraded( - repository_root, - gateway, - account, - now_utc, - ( - "dumbmoney_command_feed_refresh_missing_at_broker_sink" - if feed_refresh_missing - else dumbmoney_controls.fail_closed_reason - or "dumbmoney_command_feed_paused" - ), - False, - effective_kill_active=False, - ) - result = {**base, **degraded} - write_latest_cycle(repository_root, result) - return result - verified_authority: VerifiedCapitalAuthority | None = None - if effective_authority is not None: - try: - verified_authority = effective_authority.verify( - now_utc, - account.account_hash, - ) - activate_dumbmoney_fence( - repository_root, - verified_authority.envelope.envelope_id, - verified_authority.event_id, - verified_authority.body_digest, - verified_authority.envelope.fencing_generation, - verified_authority.envelope.policy_epoch, - ) - except (DumbMoneyAuthorityError, LiveBranchLedgerError, ValueError) as error: - degraded = _run_cancel_only_degraded( - repository_root, - gateway, - account, - now_utc, - f"capital_authority_invalid:{error}", - dumbmoney_kill_path(repository_root).is_file(), - effective_kill_active=effective_kill_active, - ) - result = {**base, **degraded} - write_latest_cycle(repository_root, result) - return result reconciled_orders = _reconcile_live_order_statuses(repository_root, account) legacy_twin_observations = _complete_legacy_twin_observations(repository_root) owner_liquidation_settlements = _settle_owner_liquidations( @@ -1695,27 +1049,6 @@ def run_live_branch_cycle( market_date, control.options_execution_enabled, ) - if ( - verified_authority is not None - and dumbmoney_kill_path(repository_root).is_file() - ): - degraded = _run_cancel_only_degraded( - repository_root, - gateway, - account, - now_utc, - "dumbmoney_kill_active", - True, - effective_kill_active=True, - ) - result = { - **base, - **degraded, - "capital_authority": verified_authority.evidence(), - "reconciled_order_status_count": len(reconciled_orders), - } - write_latest_cycle(repository_root, result) - return result if not execute_orders or not regular_market_open: result = { **base, @@ -1737,11 +1070,6 @@ def run_live_branch_cycle( "options_execution_enabled": control.options_execution_enabled, "reconciled_order_status_count": len(reconciled_orders), "order_history_blocker": history_blocker, - "capital_authority": ( - verified_authority.evidence() - if verified_authority is not None - else None - ), "orders": [], } write_latest_cycle(repository_root, result) @@ -1749,24 +1077,6 @@ def run_live_branch_cycle( unresolved_submission_present = ( history_blocker == "local_live_order_submission_unresolved" ) - if verified_authority is not None and history_blocker: - degraded = _run_cancel_only_degraded( - repository_root, - gateway, - account, - now_utc, - f"order_history_blocker:{history_blocker}", - False, - ) - result = { - **base, - **degraded, - "capital_authority": verified_authority.evidence(), - "order_history_blocker": history_blocker, - "reconciled_order_status_count": len(reconciled_orders), - } - write_latest_cycle(repository_root, result) - return result if history_blocker and not unresolved_submission_present: result = { **base, @@ -1825,37 +1135,6 @@ def run_live_branch_cycle( } write_latest_cycle(repository_root, result) return result - entry_cash_usd = settled_funds.spendable_settled_usd - witnessed_daily_loss_cents: int | None = None - if verified_authority is not None: - try: - witnessed_daily_loss_cents = _witnessed_daily_loss_cents( - repository_root, - account, - market_date, - ) - entry_cash_usd = authorized_settled_cash_usd( - verified_authority, - account, - settled_funds.spendable_settled_usd, - witnessed_daily_loss_cents, - ) - except DumbMoneyAuthorityError as error: - degraded = _run_cancel_only_degraded( - repository_root, - gateway, - account, - now_utc, - f"capital_allocation_blocked:{error}", - False, - ) - result = { - **base, - **degraded, - "capital_authority": verified_authority.evidence(), - } - write_latest_cycle(repository_root, result) - return result all_exit_intents = _build_exit_intents( repository_root, account, @@ -1870,7 +1149,7 @@ def run_live_branch_cycle( if maturity_blockers or unresolved_submission_present else build_entry_intents( repository_root, - entry_cash_usd, + settled_funds.spendable_settled_usd, now_utc.astimezone(NEW_YORK).date(), control.stock_execution_enabled, control.options_execution_enabled and regular_market_open, @@ -1897,8 +1176,6 @@ def run_live_branch_cycle( outcomes: list[dict[str, Any]] = [] for candidate_intent in intents_to_process: intent = candidate_intent - blocked_payload: dict[str, Any] - promotion_binding: VerifiedPromotionBinding | None = None day_trade_blocker = _day_trade_blocker(account, intent, market_date) if day_trade_blocker: blocked_payload = { @@ -1923,7 +1200,7 @@ def run_live_branch_cycle( intent, execution_snapshot, now_utc, - entry_cash_usd, + settled_funds.spendable_settled_usd, ) except (LiveSignalError, RobinhoodGatewayError) as error: blocked_payload = { @@ -1943,74 +1220,6 @@ def run_live_branch_cycle( ) outcomes.append(blocked_payload) continue - if intent.instrument == "option" and intent.side == "buy": - try: - validate_option_entry_for_execution(intent, now_utc) - except LiveSignalError as error: - blocked_payload = { - "intent_id": intent.intent_id, - "instrument": intent.instrument, - "symbol": intent.symbol, - "side": intent.side, - "market_date": market_date, - "status": "blocked_option_execution_snapshot_invalid", - "reason": str(error), - "intent": intent.to_dict(), - } - append_record( - ledger_path(repository_root), - "option_execution_preflight_block", - blocked_payload, - ) - outcomes.append(blocked_payload) - continue - capital_authority_evidence: dict[str, Any] | None = None - if verified_authority is not None: - try: - if intent.side == "buy": - if dumbmoney_promotion_resolver is None: - raise DumbMoneyAuthorityError( - "authenticated DumbMoney promotion resolver is absent" - ) - intent, promotion_binding = ( - dumbmoney_promotion_resolver.bind_intent( - verified_authority, - intent, - now_utc, - ) - ) - authorize_intent( - verified_authority, - account, - intent, - witnessed_daily_loss_cents, - ) - except DumbMoneyAuthorityError as error: - blocked_payload = { - "intent_id": intent.intent_id, - "instrument": intent.instrument, - "symbol": intent.symbol, - "side": intent.side, - "market_date": market_date, - "status": "blocked_dumbmoney_capital_envelope", - "reason": str(error), - "intent": intent.to_dict(), - } - append_record( - ledger_path(repository_root), - "dumbmoney_capital_preflight_block", - blocked_payload, - ) - outcomes.append(blocked_payload) - continue - capital_authority_evidence = { - **verified_authority.evidence(), - "promotion_binding": ( - promotion_binding.evidence() - if promotion_binding is not None - else None - ), - } reserved = reserve_intent( repository_root, intent.intent_id, @@ -2020,7 +1229,6 @@ def run_live_branch_cycle( "side": intent.side, "market_date": market_date, "intent": intent.to_dict(), - "capital_authority": capital_authority_evidence, }, ) if not reserved: @@ -2047,272 +1255,6 @@ def run_live_branch_cycle( intent, str(twin["record_hash"]), ) - pre_submit_guard: Callable[[], datetime] | None = None - if verified_authority is not None and effective_authority is not None: - try: - sink_now = ( - authority_clock() - if authority_clock is not None - else datetime.now(timezone.utc) - ) - sink_authority = effective_authority.verify( - sink_now, - account.account_hash, - ) - if sink_authority.event_id != verified_authority.event_id: - raise DumbMoneyAuthorityError( - "Capital authority changed during the execution cycle" - ) - assert_current_dumbmoney_fence( - repository_root, - sink_authority.envelope.envelope_id, - sink_authority.event_id, - sink_authority.body_digest, - sink_authority.envelope.fencing_generation, - sink_authority.envelope.policy_epoch, - ) - sink_account = gateway.get_account_snapshot() - if ( - not sink_account.authenticated - or sink_account.account_hash != account.account_hash - ): - raise DumbMoneyAuthorityError( - "Broker-sink account attestation changed during execution" - ) - sink_daily_loss_cents = _witnessed_daily_loss_cents( - repository_root, - sink_account, - market_date, - ) - if intent.side == "buy": - if ( - dumbmoney_promotion_resolver is None - or promotion_binding is None - ): - raise DumbMoneyAuthorityError( - "authenticated DumbMoney promotion binding is absent" - ) - intent, promotion_binding = ( - dumbmoney_promotion_resolver.bind_intent( - sink_authority, - intent, - sink_now, - expected_binding=promotion_binding, - ) - ) - authorize_intent( - sink_authority, - sink_account, - intent, - sink_daily_loss_cents, - ) - if intent.instrument == "option" and intent.side == "buy": - validate_option_entry_for_execution(intent, sink_now) - account = sink_account - capital_authority_evidence = { - **sink_authority.evidence(), - "verified_again_at_broker_sink": True, - "verified_at": sink_now.astimezone(timezone.utc).isoformat(), - "witnessed_daily_loss_cents": sink_daily_loss_cents, - "promotion_binding": ( - promotion_binding.evidence() - if promotion_binding is not None - else None - ), - } - - def final_broker_sink_guard( - *, - expected_event_id: str = sink_authority.event_id, - expected_envelope_id: str = (sink_authority.envelope.envelope_id), - expected_body_digest: str = sink_authority.body_digest, - expected_generation: int = ( - sink_authority.envelope.fencing_generation - ), - expected_policy_epoch: int = (sink_authority.envelope.policy_epoch), - expected_account_hash: str = sink_account.account_hash, - guarded_intent: LiveExecutionIntent = intent, - expected_promotion_binding: ( - VerifiedPromotionBinding | None - ) = promotion_binding, - ) -> datetime: - guard_now = ( - authority_clock() - if authority_clock is not None - else datetime.now(timezone.utc) - ) - if dumbmoney_kill_path(repository_root).is_file(): - raise RobinhoodGatewayError( - "Local DumbMoney kill became active before broker submission" - ) - if dumbmoney_controls is not None and ( - dumbmoney_controls.kill_active - or not dumbmoney_controls.allow_new_risk - ): - raise RobinhoodGatewayError( - "DumbMoney command controls deny broker submission" - ) - try: - refreshed_context = effective_authority - if dumbmoney_control_refresh is not None: - try: - refreshed_controls = dumbmoney_control_refresh( - guard_now - ) - except Exception as error: - raise DumbMoneyAuthorityError( - "DumbMoney command refresh failed closed: " - f"{type(error).__name__}" - ) from error - if not isinstance( - refreshed_controls, - DumbMoneyCommandDispatch, - ): - raise DumbMoneyAuthorityError( - "DumbMoney command refresh returned an " - "unsupported control snapshot" - ) - if ( - refreshed_controls.kill_active - or not refreshed_controls.allow_new_risk - or refreshed_controls.capital_authority is None - ): - raise DumbMoneyAuthorityError( - "Refreshed DumbMoney command controls deny " - "broker submission" - ) - refreshed_context = refreshed_controls.capital_authority - if refreshed_context is None: - raise DumbMoneyAuthorityError( - "Final DumbMoney capital authority is absent" - ) - guarded_authority = refreshed_context.verify( - guard_now, - expected_account_hash, - ) - guarded_identity = ( - guarded_authority.envelope.envelope_id, - guarded_authority.event_id, - guarded_authority.body_digest, - guarded_authority.envelope.fencing_generation, - guarded_authority.envelope.policy_epoch, - ) - expected_identity = ( - expected_envelope_id, - expected_event_id, - expected_body_digest, - expected_generation, - expected_policy_epoch, - ) - if guarded_identity != expected_identity: - raise DumbMoneyAuthorityError( - "Capital authority changed before broker submission" - ) - assert_current_dumbmoney_fence( - repository_root, - guarded_authority.envelope.envelope_id, - guarded_authority.event_id, - guarded_authority.body_digest, - guarded_authority.envelope.fencing_generation, - guarded_authority.envelope.policy_epoch, - ) - guarded_binding = expected_promotion_binding - if guarded_intent.side == "buy": - if ( - dumbmoney_promotion_resolver is None - or expected_promotion_binding is None - ): - raise DumbMoneyAuthorityError( - "Final authenticated promotion binding is absent" - ) - _, guarded_binding = ( - dumbmoney_promotion_resolver.bind_intent( - guarded_authority, - guarded_intent, - guard_now, - expected_binding=expected_promotion_binding, - ) - ) - authorize_intent( - guarded_authority, - sink_account, - guarded_intent, - sink_daily_loss_cents, - ) - if ( - guarded_intent.instrument == "option" - and guarded_intent.side == "buy" - ): - validate_option_entry_for_execution( - guarded_intent, - guard_now, - ) - capital_deadline = datetime.fromisoformat( - guarded_authority.envelope.expires_at.replace( - "Z", - "+00:00", - ) - ) - if guarded_binding is None: - return capital_deadline - promotion_deadline = datetime.fromisoformat( - guarded_binding.expires_at.replace("Z", "+00:00") - ) - return min(capital_deadline, promotion_deadline) - except ( - DumbMoneyAuthorityError, - LiveBranchLedgerError, - LiveSignalError, - ) as error: - raise RobinhoodGatewayError( - "Final DumbMoney broker-sink guard denied submission: " - f"{error}" - ) from error - - pre_submit_guard = final_broker_sink_guard - except ( - DumbMoneyAuthorityError, - LiveBranchLedgerError, - LiveSignalError, - RobinhoodGatewayError, - ) as error: - blocked_payload = { - "intent_id": intent.intent_id, - "instrument": intent.instrument, - "symbol": intent.symbol, - "side": intent.side, - "market_date": market_date, - "paper_twin_record_hash": twin["record_hash"], - "paper_twin_execution_record_hash": paper_twin_execution[ - "record_hash" - ], - "intent": intent.to_dict(), - "capital_authority": capital_authority_evidence, - "review_succeeded": False, - "placed": False, - "order_id": "", - "status": "blocked_dumbmoney_broker_sink_authority", - "warnings": [], - "error": str(error), - "observed_tools": [], - } - live_record = append_record( - ledger_path(repository_root), - "live_order_result", - blocked_payload, - ) - _append_paper_twin_outcome( - repository_root, - intent.intent_id, - str(twin["record_hash"]), - str(live_record["record_hash"]), - "blocked_dumbmoney_broker_sink_authority", - False, - None, - None, - ) - outcomes.append(blocked_payload) - continue append_record( ledger_path(repository_root), "live_order_submission_started", @@ -2323,121 +1265,14 @@ def final_broker_sink_guard( "side": intent.side, "market_date": market_date, "paper_twin_record_hash": twin["record_hash"], - "paper_twin_execution_record_hash": paper_twin_execution["record_hash"], + "paper_twin_execution_record_hash": paper_twin_execution[ + "record_hash" + ], "intent": intent.to_dict(), - "capital_authority": capital_authority_evidence, }, ) - if pre_submit_guard is not None: - append_record( - ledger_path(repository_root), - "dumbmoney_order_dispatching", - { - "intent_id": intent.intent_id, - "instrument": intent.instrument, - "symbol": intent.symbol, - "market_date": market_date, - "status": "DISPATCHING", - "external_write_boundary": ( - "single_at_most_once_deterministic_robinhood_place_call" - ), - "placement_retries_allowed": 0, - "capital_authority": capital_authority_evidence, - }, - ) try: - if pre_submit_guard is None: - order_result = gateway.place_order( - intent, - str(twin["record_hash"]), - ) - else: - order_result = gateway.place_order( - intent, - str(twin["record_hash"]), - pre_submit_guard=pre_submit_guard, - ) - in_flight_revocation_error = "" - if pre_submit_guard is not None: - try: - pre_submit_guard() - except RobinhoodGatewayError as error: - in_flight_revocation_error = str(error) - if in_flight_revocation_error: - ambiguous_payload = { - "intent_id": intent.intent_id, - "instrument": intent.instrument, - "symbol": intent.symbol, - "side": intent.side, - "market_date": market_date, - "paper_twin_record_hash": twin["record_hash"], - "paper_twin_execution_record_hash": paper_twin_execution[ - "record_hash" - ], - "intent": intent.to_dict(), - "review_succeeded": order_result.review_succeeded, - "placed": None, - "order_id": order_result.order_id, - "status": "ambiguous_in_flight_authority_revoked", - "warnings": list(order_result.warnings), - "error": in_flight_revocation_error, - "observed_tools": list(order_result.observed_tools), - "capital_authority": capital_authority_evidence, - "placement_retried": False, - "in_flight_authority_revoked": True, - "broker_result_before_revocation_detection": ( - order_result.to_dict() - ), - } - ambiguous_record = append_record( - ledger_path(repository_root), - "live_order_submission_ambiguous", - ambiguous_payload, - ) - cancel_reconciliation: dict[str, Any] - try: - revocation_account = gateway.get_account_snapshot() - except RobinhoodGatewayError as error: - cancel_reconciliation = { - "cycle_status": "cancel_only_degraded", - "authority_state": "CANCEL_ONLY_DEGRADED", - "cancel_only_reason": ( - "in_flight_authority_revoked_account_poll_failed" - ), - "new_exposure_allowed": False, - "kill_active": True, - "cancel_requested": False, - "history_poll_errors": [str(error)], - "orders": [], - } - else: - cancel_reconciliation = _run_cancel_only_degraded( - repository_root, - gateway, - revocation_account, - datetime.now(timezone.utc), - "in_flight_authority_revoked", - True, - effective_kill_active=True, - ) - account = AccountSnapshot(**cancel_reconciliation["account"]) - _append_paper_twin_outcome( - repository_root, - intent.intent_id, - str(twin["record_hash"]), - str(ambiguous_record["record_hash"]), - "ambiguous_in_flight_authority_revoked", - None, - None, - None, - ) - outcomes.append( - { - **ambiguous_payload, - "cancel_reconciliation": cancel_reconciliation, - } - ) - continue + order_result = gateway.place_order(intent, str(twin["record_hash"])) except RobinhoodPlacementAmbiguousError as error: ambiguous_payload = { "intent_id": intent.intent_id, @@ -2446,7 +1281,9 @@ def final_broker_sink_guard( "side": intent.side, "market_date": market_date, "paper_twin_record_hash": twin["record_hash"], - "paper_twin_execution_record_hash": paper_twin_execution["record_hash"], + "paper_twin_execution_record_hash": paper_twin_execution[ + "record_hash" + ], "intent": intent.to_dict(), "review_succeeded": False, "placed": None, @@ -2455,8 +1292,6 @@ def final_broker_sink_guard( "warnings": [], "error": str(error), "observed_tools": [], - "capital_authority": capital_authority_evidence, - "placement_retried": False, } ambiguous_record = append_record( ledger_path(repository_root), @@ -2485,45 +1320,7 @@ def final_broker_sink_guard( None, None, ) - reconciled_payload: dict[str, Any] | None = None - history_poll_count = 0 - if verified_authority is not None: - reconciled_payload, polled_account, history_poll_count = ( - _poll_ambiguous_placement_history( - repository_root, - gateway, - intent, - market_date, - sink_now, - str(twin["record_hash"]), - ) - ) - if polled_account is not None: - account = polled_account - if reconciled_payload is None: - outcomes.append( - { - **ambiguous_payload, - "automatic_history_poll_count": history_poll_count, - "authority_state": ( - "CANCEL_ONLY_DEGRADED" - if verified_authority is not None - else "LEGACY_MANUAL_RECONCILIATION" - ), - } - ) - else: - _append_paper_twin_outcome( - repository_root, - intent.intent_id, - str(twin["record_hash"]), - str(reconciled_payload["automatic_history_record_hash"]), - str(reconciled_payload["status"]), - True, - float(reconciled_payload["filled_quantity"]), - float(reconciled_payload["average_fill_price"]), - ) - outcomes.append(reconciled_payload) + outcomes.append(ambiguous_payload) continue except RobinhoodGatewayError as error: blocked_payload = { @@ -2533,7 +1330,9 @@ def final_broker_sink_guard( "side": intent.side, "market_date": market_date, "paper_twin_record_hash": twin["record_hash"], - "paper_twin_execution_record_hash": paper_twin_execution["record_hash"], + "paper_twin_execution_record_hash": paper_twin_execution[ + "record_hash" + ], "intent": intent.to_dict(), "review_succeeded": False, "placed": False, @@ -2610,20 +1409,13 @@ def final_broker_sink_guard( ) outcomes.append(live_payload) blocked_order_count = sum( - str(outcome.get("status", "")).startswith(("blocked_", "ambiguous_")) - for outcome in outcomes - ) - dumbmoney_cancel_only = verified_authority is not None and any( - outcome.get("authority_state") == "CANCEL_ONLY_DEGRADED" - or outcome.get("status") == "ambiguous_in_flight_authority_revoked" + str(outcome.get("status", "")).startswith(("blocked_", "ambiguous_submission_")) for outcome in outcomes ) result = { **base, "cycle_status": ( - "cancel_only_degraded" - if dumbmoney_cancel_only - else "complete_exits_only_unresolved_submission_entries_blocked" + "complete_exits_only_unresolved_submission_entries_blocked" if unresolved_submission_present and exit_intents else "blocked_unresolved_submission_no_exit_intents" if unresolved_submission_present @@ -2642,20 +1434,6 @@ def final_broker_sink_guard( "options_execution_enabled": control.options_execution_enabled, "use_full_available_balance": control.use_full_available_balance, "settled_funds": settled_funds.to_dict(), - "dumbmoney_authorized_entry_cash_usd": ( - entry_cash_usd if verified_authority is not None else None - ), - "witnessed_daily_loss_cents": witnessed_daily_loss_cents, - "capital_authority": ( - verified_authority.evidence() if verified_authority is not None else None - ), - "authority_state": ( - "CANCEL_ONLY_DEGRADED" - if dumbmoney_cancel_only - else "ACTIVE" - if verified_authority is not None - else "LEGACY_DISABLED" - ), "reconciled_order_status_count": len(reconciled_orders), "legacy_twin_observation_count": len(legacy_twin_observations), "owner_liquidation_settlement_count": len(owner_liquidation_settlements), @@ -2678,8 +1456,4 @@ def final_broker_sink_guard( def default_gateway() -> RobinhoodAgenticGateway: - return RobinhoodAgenticGateway( - subprocess.run, - 60, - write_timeout_seconds=30, - ) + return RobinhoodAgenticGateway(subprocess.run, 60) diff --git a/dopey_live/signals.py b/dopey_live/signals.py index 1508de2..b0a28db 100644 --- a/dopey_live/signals.py +++ b/dopey_live/signals.py @@ -10,7 +10,7 @@ from datetime import date, datetime, timedelta, timezone from decimal import ROUND_DOWN, ROUND_UP, Decimal from pathlib import Path -from typing import Any, Final, cast +from typing import Any, Final from zoneinfo import ZoneInfo import exchange_calendars as xcals @@ -22,7 +22,6 @@ ) from .models import ( - AnalysisRelationship, EQUITY_SNAPSHOT_SOURCE, EquityExecutionSnapshot, EquityQuoteSnapshot, @@ -46,7 +45,9 @@ ACTION_READINESS_ARTIFACT_TYPE: Final[str] = "action_readiness_gate" RELEASE_GATE_VERSION: Final[str] = "dopey-live-pivot-release-gate-v1" CALIBRATION_VERSION: Final[str] = "dopey-vnext-calibration-v2" -CHAMPION_HARDENING_VERSION: Final[str] = "dopey-public-champion-hardening-gate-v1" +CHAMPION_HARDENING_VERSION: Final[str] = ( + "dopey-public-champion-hardening-gate-v1" +) MAX_GATE_RECOMPUTE_AGE: Final[timedelta] = timedelta(minutes=5) MAX_RESEARCH_INPUT_AGE: Final[timedelta] = timedelta(hours=36) MAX_ARTIFACT_FUTURE_SKEW: Final[timedelta] = timedelta(seconds=5) @@ -273,7 +274,8 @@ def _artifact_integrity_blockers( and fitness_determinism.get("deterministic") is True ) hardened_champion_available = bool( - champion.get("artifact_type") == "public_champion_hardening_gate_v1_summary" + champion.get("artifact_type") + == "public_champion_hardening_gate_v1_summary" and champion.get("public_champion_hardening_gate_version") == CHAMPION_HARDENING_VERSION and type(champion.get("hardened_champion_count")) is int @@ -313,12 +315,15 @@ def _artifact_integrity_blockers( "action_release_binding_invalid", ), ( - calibration.get("artifact_type") == "vnext_probability_calibration_summary" + calibration.get("artifact_type") + == "vnext_probability_calibration_summary" and calibration.get("calibration_version") == CALIBRATION_VERSION, "forward_calibration_schema_invalid", ), ( - SHA256_PATTERN.fullmatch(str(calibration.get("reproducibility_hash", ""))) + SHA256_PATTERN.fullmatch( + str(calibration.get("reproducibility_hash", "")) + ) is not None and calibration.get("reproducibility_hash") == _canonical_summary_hash(calibration), @@ -338,13 +343,15 @@ def _artifact_integrity_blockers( "forward_calibration_stale", ), ( - champion.get("artifact_type") == "public_champion_hardening_gate_v1_summary" + champion.get("artifact_type") + == "public_champion_hardening_gate_v1_summary" and champion.get("public_champion_hardening_gate_version") == CHAMPION_HARDENING_VERSION, "champion_hardening_schema_invalid", ), ( - SHA256_PATTERN.fullmatch(str(champion.get("summary_hash", ""))) is not None + SHA256_PATTERN.fullmatch(str(champion.get("summary_hash", ""))) + is not None and champion.get("summary_hash") == _stable_summary_hash(champion), "champion_hardening_hash_invalid", ), @@ -366,7 +373,8 @@ def _artifact_integrity_blockers( "nightly_schema_invalid", ), ( - SHA256_PATTERN.fullmatch(str(nightly.get("summary_hash", ""))) is not None + SHA256_PATTERN.fullmatch(str(nightly.get("summary_hash", ""))) + is not None and nightly.get("summary_hash") == _stable_summary_hash(nightly), "nightly_hash_invalid", ), @@ -384,7 +392,8 @@ def _artifact_integrity_blockers( "release_champion_binding_invalid", ), ( - release_checks_object.get("full_nightly_validation_green") is nightly_green, + release_checks_object.get("full_nightly_validation_green") + is nightly_green, "release_nightly_binding_invalid", ), ( @@ -404,7 +413,9 @@ def _artifact_integrity_blockers( def research_maturity_blockers(repository_root: Path) -> tuple[str, ...]: artifact_root = repository_root.resolve() / "artifacts" / "dopey" action_path = artifact_root / "broker_readiness" / "action_readiness_gate.json" - release_path = artifact_root / "broker_readiness" / "live_pivot_release_gate.json" + release_path = ( + artifact_root / "broker_readiness" / "live_pivot_release_gate.json" + ) calibration_path = ( artifact_root / "vnext" @@ -740,22 +751,11 @@ def prepare_equity_entry_for_execution( ) if notional_usd <= 0.0: raise LiveSignalError("Equity limit order has no positive risk-capped notional") - quantity = float( - (Decimal(str(notional_usd)) / Decimal(str(limit_price))).quantize( - Decimal("0.000001"), - rounding=ROUND_DOWN, - ) - ) - if quantity <= 0.0: - raise LiveSignalError( - "Equity limit order has no positive deterministic share quantity" - ) return replace( intent, order_type="limit", limit_price=limit_price, notional_usd=notional_usd, - quantity=quantity, execution_snapshot=snapshot, reason_codes=( *intent.reason_codes, @@ -1038,10 +1038,7 @@ def build_leveraged_etf_entry_intents( source_hash=source_hash, thesis_direction=str(plan.get("exposure_direction", "")), analysis_symbol=analysis_symbol, - analysis_relationship=cast( - AnalysisRelationship, - str(plan.get("analysis_relationship", "")), - ), + analysis_relationship=str(plan.get("analysis_relationship", "")), analysis_reference_price=float(entry_plan["analysis_reference_price"]), analysis_invalidation_price=float( exit_plan["analysis_invalidation_price"] @@ -1150,18 +1147,10 @@ def build_option_entry_intents( direction = str(thesis.get("direction", "")) if (direction, option_type) not in {("bullish", "call"), ("bearish", "put")}: continue - try: - ask = float(primary.get("ask", 0.0)) - multiplier = int(primary.get("multiplier", 0)) - expiry = date.fromisoformat(str(primary.get("expiry", ""))) - except (TypeError, ValueError): - continue + ask = float(primary.get("ask", 0.0)) contract_score = float(primary.get("contract_score", 0.0)) if ( ask <= 0.0 - or not math.isfinite(ask) - or multiplier != 100 - or expiry < market_date or contract_score <= 0.0 or str(primary.get("hard_reject_reason", "")) or not _option_quote_matches_market_session(primary, market_date) @@ -1184,7 +1173,6 @@ def build_option_entry_intents( continue option_contract = { "contract_symbol_key": str(primary.get("contract_symbol_key", "")), - "contract_id": str(primary.get("contract_id", "")), "expiry": str(primary.get("expiry", "")), "option_type": str( primary.get("option_type", selection.get("option_type", "")) @@ -1194,7 +1182,6 @@ def build_option_entry_intents( "order_action": "buy_to_open", "defined_risk": True, "quote_observed_at": str(primary["quote_observed_at"]), - "multiplier": multiplier, "maximum_loss_usd": round(premium_per_contract * quantity, 2), } source = {"thesis": thesis, "contract": selection} @@ -1249,64 +1236,6 @@ def build_option_entry_intents( return tuple(intents) -def validate_option_entry_for_execution( - intent: LiveExecutionIntent, - now_utc: datetime, - maximum_quote_age: timedelta = timedelta(seconds=60), -) -> None: - """Require a fresh, exact, fully bounded long-premium option debit.""" - if intent.instrument != "option" or intent.side != "buy": - raise LiveSignalError( - "Option execution validation is limited to long-premium entries" - ) - if now_utc.tzinfo is None: - raise LiveSignalError("Option execution validation requires aware now_utc") - contract = intent.option_contract - try: - observed_at = datetime.fromisoformat( - str(contract.get("quote_observed_at", "")).replace("Z", "+00:00") - ) - expiry = date.fromisoformat(str(contract.get("expiry", ""))) - multiplier = int(contract.get("multiplier", 0)) - maximum_loss = Decimal(str(contract.get("maximum_loss_usd", ""))) - exact_debit = ( - Decimal(str(intent.limit_price)) - * Decimal(str(intent.quantity)) - * Decimal(multiplier) - ) - notional = Decimal(str(intent.notional_usd)) - except (ValueError, TypeError, ArithmeticError) as error: - raise LiveSignalError( - "Option execution contract has invalid quote, expiry, multiplier, or debit fields" - ) from error - if observed_at.tzinfo is None: - raise LiveSignalError("Option execution quote must be timezone-aware") - quote_age = now_utc.astimezone(timezone.utc) - observed_at.astimezone(timezone.utc) - if quote_age < timedelta(seconds=-5) or quote_age > maximum_quote_age: - raise LiveSignalError( - "Option execution quote is outside the 60-second freshness window" - ) - if expiry < now_utc.astimezone(NEW_YORK).date(): - raise LiveSignalError("Option contract is expired") - if multiplier != 100: - raise LiveSignalError("Option contract multiplier must be exactly 100") - if ( - intent.order_type != "limit" - or intent.limit_price <= 0.0 - or intent.quantity <= 0.0 - or not str(contract.get("contract_id", "")).strip() - or contract.get("defined_risk") is not True - or contract.get("order_action") != "buy_to_open" - ): - raise LiveSignalError( - "Option entry must be an exact defined-risk buy-to-open limit order" - ) - if exact_debit <= 0 or maximum_loss != exact_debit or notional != exact_debit: - raise LiveSignalError( - "Option max loss and notional must equal exact limit debit" - ) - - def build_entry_intents( repository_root: Path, buying_power_usd: float, diff --git a/operator_tools/install_live_branch_schedule.ps1 b/operator_tools/install_live_branch_schedule.ps1 index b8b13b5..46484dd 100644 --- a/operator_tools/install_live_branch_schedule.ps1 +++ b/operator_tools/install_live_branch_schedule.ps1 @@ -5,13 +5,9 @@ $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest $repoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path -$projectMarker = Join-Path $repoRoot "pyproject.toml" -$commandMarker = Join-Path $repoRoot "operator_tools\live_branch.cmd" -if ( - -not (Test-Path -LiteralPath $projectMarker -PathType Leaf) -or - -not (Test-Path -LiteralPath $commandMarker -PathType Leaf) -) { - throw "Resolved repository root lacks required DOPEY release markers: root=$repoRoot" +$expectedRoot = "C:\Users\CH\dopey" +if (-not $repoRoot.Equals($expectedRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Repository root mismatch: expected=$expectedRoot actual=$repoRoot" } $systemRoot = [Environment]::GetEnvironmentVariable("SystemRoot") diff --git a/operator_tools/live_branch.cmd b/operator_tools/live_branch.cmd index 2b02e5a..41b640f 100644 --- a/operator_tools/live_branch.cmd +++ b/operator_tools/live_branch.cmd @@ -1,6 +1,6 @@ @echo off -rem DOPEY Live Branch legacy read-only observation cycle. Production execution -rem is available only through the supervised DumbMoney Windows service. +rem DOPEY Live Branch production cycle. This launcher is intentionally separate +rem from every paper-research command and control surface. setlocal set ROOT=%~dp0.. cd /d "%ROOT%" @@ -9,7 +9,7 @@ set LOG_FILE=%LOG_DIR%\live_branch_scheduler.log if not exist "%LOG_DIR%" mkdir "%LOG_DIR%" for %%A in ("%LOG_FILE%") do if %%~zA GTR 1048576 move /y "%LOG_FILE%" "%LOG_FILE%.old" >nul echo ==== DOPEY Live Branch %date% %time% ==== >> "%LOG_FILE%" -"%ROOT%\.venv\Scripts\python.exe" -m operator_tools.operator_live_branch >> "%LOG_FILE%" 2>&1 +"%ROOT%\.venv\Scripts\python.exe" -m operator_tools.operator_live_branch --execute >> "%LOG_FILE%" 2>&1 set LIVE_RC=%errorlevel% echo ==== Live Branch complete rc=%LIVE_RC% %time% ==== >> "%LOG_FILE%" exit /b %LIVE_RC% diff --git a/operator_tools/operator_live_branch.py b/operator_tools/operator_live_branch.py index df7c508..00bc905 100644 --- a/operator_tools/operator_live_branch.py +++ b/operator_tools/operator_live_branch.py @@ -1,5 +1,4 @@ """Run or preflight the DOPEY Live Branch.""" - from __future__ import annotations import argparse @@ -9,11 +8,8 @@ from contextlib import contextmanager from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Callable, Iterator +from typing import Iterator -from dopey_live.dumbmoney_authority import DumbMoneyAuthorityContext -from dopey_live.dumbmoney_command_feed import DumbMoneyCommandDispatch -from dopey_live.dumbmoney_promotion import PromotionAuthorityResolver from dopey_live.ledger import write_latest_cycle from dopey_live.service import default_gateway, run_live_branch_cycle @@ -127,50 +123,16 @@ def _exclusive_live_process(now_utc: datetime) -> Iterator[None]: lock_path.unlink() -def _run_with_crash_tombstone( - execute_orders: bool, - dumbmoney_authority: DumbMoneyAuthorityContext | None = None, - *, - dumbmoney_controls: DumbMoneyCommandDispatch | None = None, - dumbmoney_control_refresh: ( - Callable[[datetime], DumbMoneyCommandDispatch] | None - ) = None, - dumbmoney_promotion_resolver: PromotionAuthorityResolver | None = None, -) -> dict[str, object]: - if execute_orders: - raise RuntimeError( - "Legacy operator execution is disabled; use the supervised " - "DumbMoney Windows service" - ) +def _run_with_crash_tombstone(execute_orders: bool) -> dict[str, object]: started_at_utc = datetime.now(timezone.utc) try: with _exclusive_live_process(started_at_utc): - resolved_controls = dumbmoney_controls - if ( - resolved_controls is None - and dumbmoney_authority is None - and dumbmoney_control_refresh is not None - ): - resolved_controls = dumbmoney_control_refresh(started_at_utc) - arguments = ( + return run_live_branch_cycle( REPOSITORY_ROOT, default_gateway(), started_at_utc, execute_orders, ) - if ( - dumbmoney_authority is None - and resolved_controls is None - and dumbmoney_control_refresh is None - ): - return run_live_branch_cycle(*arguments) - return run_live_branch_cycle( - *arguments, - dumbmoney_authority=dumbmoney_authority, - dumbmoney_controls=resolved_controls, - dumbmoney_control_refresh=dumbmoney_control_refresh, - dumbmoney_promotion_resolver=dumbmoney_promotion_resolver, - ) except Exception as error: tombstone: dict[str, object] = { "schema_version": "dopey-live-branch-cycle-v2", @@ -194,92 +156,11 @@ def _run_with_crash_tombstone( raise -def run_supervised_dumbmoney_cycle( - command_feed_poll: Callable[[datetime], DumbMoneyCommandDispatch], - promotion_resolver: PromotionAuthorityResolver, - *, - execute_orders: bool, -) -> dict[str, object]: - """Run one process-locked cycle with initial and broker-boundary feed polls.""" - - return _run_with_crash_tombstone( - execute_orders, - dumbmoney_control_refresh=command_feed_poll, - dumbmoney_promotion_resolver=promotion_resolver, - ) - - def main() -> int: - parser = argparse.ArgumentParser( - description="Operate DOPEY Live Branch through official Robinhood Agentic MCP" - ) - parser.add_argument( - "--execute", - action="store_true", - help=( - "Disabled legacy switch; production execution requires the " - "supervised DumbMoney Windows service" - ), - ) - parser.add_argument( - "--dumbmoney-strict", - action="store_true", - help=( - "Require complete DumbMoney authority inputs for read-only legacy preflight" - ), - ) - parser.add_argument( - "--dumbmoney-envelope", - type=Path, - help="Opt in to DumbMoney authority with this signed CapitalEnvelopeV1", - ) - parser.add_argument( - "--dumbmoney-public-key", - type=Path, - help="Trusted DumbMoney Ed25519 public key (raw, hex, or base64url)", - ) - parser.add_argument( - "--dumbmoney-account-hash", - help="Pinned lowercase SHA-256 of the dedicated Agentic account ID", - ) + parser = argparse.ArgumentParser(description="Operate DOPEY Live Branch through official Robinhood Agentic MCP") + parser.add_argument("--execute", action="store_true", help="Submit eligible reviewed orders while the market is open") arguments = parser.parse_args() - if arguments.execute: - parser.error( - "--execute is disabled in the legacy operator; use " - "dopey_live.dumbmoney_windows_service" - ) - dumbmoney_values = ( - arguments.dumbmoney_envelope, - arguments.dumbmoney_public_key, - arguments.dumbmoney_account_hash, - ) - if any(value is not None for value in dumbmoney_values) and not all( - value is not None for value in dumbmoney_values - ): - parser.error( - "DumbMoney mode requires --dumbmoney-envelope, " - "--dumbmoney-public-key, and --dumbmoney-account-hash together" - ) - if arguments.dumbmoney_strict and not all( - value is not None for value in dumbmoney_values - ): - parser.error( - "--dumbmoney-strict requires the complete DumbMoney envelope, " - "public key, and account hash" - ) - authority = ( - DumbMoneyAuthorityContext.from_files( - arguments.dumbmoney_envelope, - arguments.dumbmoney_public_key, - arguments.dumbmoney_account_hash, - ) - if all(value is not None for value in dumbmoney_values) - else None - ) - result = _run_with_crash_tombstone( - False, - authority, - ) + result = _run_with_crash_tombstone(bool(arguments.execute)) print(json.dumps(result, indent=2, sort_keys=True, ensure_ascii=False)) cycle_status = str(result.get("cycle_status", "")) successful_statuses = { diff --git a/tests/_dummy_v2_manifest.txt b/tests/_dummy_v2_manifest.txt deleted file mode 100644 index 21aa0d1..0000000 --- a/tests/_dummy_v2_manifest.txt +++ /dev/null @@ -1 +0,0 @@ -2E08B856193A6542D3B19CD5EE6EE9C6D1B7B607DC73DC875F75B2F5933DD59D C:\src\engine\dopey-dumbmoney\tests\dummy_blunder.py diff --git a/tests/dummy_blunder.py b/tests/dummy_blunder.py deleted file mode 100644 index 4a504b4..0000000 --- a/tests/dummy_blunder.py +++ /dev/null @@ -1 +0,0 @@ -# original blunder content diff --git a/tests/fixtures/dumbmoney/capital-envelope-cap-order-negative.v1.json b/tests/fixtures/dumbmoney/capital-envelope-cap-order-negative.v1.json deleted file mode 100644 index 9a771ef..0000000 --- a/tests/fixtures/dumbmoney/capital-envelope-cap-order-negative.v1.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "fixture_schema": "dopey.dumbmoney-capital-envelope-negative-cases.v1", - "cases": [ - { - "name": "max_order_exceeds_max_correlated", - "overrides": { - "max_order_risk_cents": 301, - "max_correlated_risk_cents": 300, - "max_open_risk_cents": 1250 - }, - "expected_error": "max_order_risk_cents <= max_correlated_risk_cents <= max_open_risk_cents" - }, - { - "name": "max_correlated_exceeds_max_open", - "overrides": { - "max_order_risk_cents": 100, - "max_correlated_risk_cents": 1251, - "max_open_risk_cents": 1250 - }, - "expected_error": "max_order_risk_cents <= max_correlated_risk_cents <= max_open_risk_cents" - } - ] -} diff --git a/tests/fixtures/dumbmoney/signed-capital-envelope.v1.json b/tests/fixtures/dumbmoney/signed-capital-envelope.v1.json deleted file mode 100644 index 7327a1d..0000000 --- a/tests/fixtures/dumbmoney/signed-capital-envelope.v1.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "fixture_schema": "dumbmoney.signed-capital-envelope-fixture.v1", - "public_key_base64url": "A6EHv_POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg", - "signing_payload_sha256": "97f8ecdf0d6b48b310c06cdeb37654fb171f4fa62bf91b427965191643d7ee1e", - "envelope": { - "schema": "dumbmoney.signed-envelope.v1", - "source_id": "dumbmoney-core", - "source_sequence": 1, - "event_id": "95fd19cc7bcbc89fe3c7985ee04d502704344b2f09c1eb6dce575b41c0ae579c", - "correlation_id": "fixture-capital-001", - "causation_id": null, - "nonce": "fixture-nonce-001", - "not_before": "2099-01-01T00:00:00Z", - "expires_at": "2099-01-01T00:01:00Z", - "body_schema": "dumbmoney.capital-envelope.v1", - "body_digest": "fcf86b4c257c0fea9ba65051366b76738563598d4217c4950ef3cf63ed1927d0", - "body": { - "schema": "dumbmoney.capital-envelope.v1", - "envelope_id": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "mandate_id": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", - "venue": "dopey_robinhood", - "account_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "strategy_hashes": [ - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - ], - "passport_hashes": [ - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - ], - "promotion_hashes": [ - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - ], - "authorized_instruments": [ - "equity:ACME" - ], - "authorized_mode": "LIVE", - "max_order_risk_cents": 100, - "max_open_risk_cents": 1250, - "max_correlated_risk_cents": 300, - "max_daily_loss_cents": 300, - "max_open_orders": 5, - "fencing_generation": 7, - "policy_epoch": 1, - "not_before": "2099-01-01T00:00:00Z", - "expires_at": "2099-01-01T00:01:00Z" - }, - "signature_algorithm": "Ed25519", - "signer_key_id": "56475aa75463474c0285df5dbf2bcab73da651358839e9b77481b2eab107708c", - "signature": "ga9nH3SEc6uGk8U_Q-tqWXa76n_-OSq8VhUAPHlNdTp5jzGmcZkN8JIjXuhpidniz4NOuTjpPWUE9vvtiqQ7CQ" - } -} diff --git a/tests/test_dumbmoney_command_feed.py b/tests/test_dumbmoney_command_feed.py deleted file mode 100644 index f8c91b1..0000000 --- a/tests/test_dumbmoney_command_feed.py +++ /dev/null @@ -1,1119 +0,0 @@ -from __future__ import annotations - -import base64 -import hashlib -import json -from datetime import datetime, timedelta, timezone -from pathlib import Path -from types import SimpleNamespace -from typing import Any, Callable -from unittest.mock import patch -from urllib.parse import parse_qs, urlsplit - -import pytest -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -import dopey_live.service as live_service -from dopey_live.dumbmoney_authority import ( - VerifiedCapitalAuthority, - canonical_json_bytes, -) -from dopey_live.dumbmoney_command_feed import ( - CommandFeedHttpRequest, - CommandFeedHttpResponse, - poll_once, -) -from dopey_live.ledger import append_record, ledger_path, read_ledger -from dopey_live.dumbmoney_promotion import VerifiedPromotionBinding -from dopey_live.models import ( - AccountSnapshot, - GatewayCancelResult, - GatewayOrderResult, - LiveExecutionIntent, -) -from dopey_live.service import run_live_branch_cycle - - -NOW = datetime(2099, 1, 1, 0, 0, 30, tzinfo=timezone.utc) -ACCOUNT_HASH = "c" * 64 -STRATEGY_HASH = "a" * 64 -PASSPORT_HASH = "d" * 64 -PROMOTION_HASH = "f" * 64 -ZERO_DIGEST = "0" * 64 -TEST_REQUEST_NONCE = "7" * 64 - - -def _format_utc(value: datetime) -> str: - rendered = value.astimezone(timezone.utc).isoformat( - timespec="microseconds" if value.microsecond else "seconds" - ) - return rendered.replace("+00:00", "Z") - - -def _sign( - private_key: Ed25519PrivateKey, - body: dict[str, Any], - *, - source_sequence: int, - not_before: datetime, - expires_at: datetime, -) -> dict[str, Any]: - public_key = private_key.public_key().public_bytes_raw() - unsigned: dict[str, Any] = { - "schema": "dumbmoney.signed-envelope.v1", - "source_id": "dumbmoney-core", - "source_sequence": source_sequence, - "correlation_id": "feed-test", - "causation_id": None, - "nonce": f"feed-test-{source_sequence}", - "not_before": _format_utc(not_before), - "expires_at": _format_utc(expires_at), - "body_schema": body["schema"], - "body_digest": hashlib.sha256(canonical_json_bytes(body)).hexdigest(), - "body": body, - "signature_algorithm": "Ed25519", - "signer_key_id": hashlib.sha256(public_key).hexdigest(), - } - event_material = dict(unsigned) - unsigned["event_id"] = hashlib.sha256( - canonical_json_bytes(event_material) - ).hexdigest() - signature = private_key.sign(canonical_json_bytes(unsigned)) - return { - **unsigned, - "signature": base64.urlsafe_b64encode(signature).decode("ascii").rstrip("="), - } - - -def _kill_body(active: bool, generation: int = 1) -> dict[str, Any]: - return { - "schema": "dumbmoney.kill-state.v1", - "active": active, - "generation": generation, - "reason": "feed test kill state", - "changed_by": "owner", - "policy_epoch": 1, - "changed_at": _format_utc(NOW - timedelta(seconds=1)), - } - - -def _mode_body(mode: str = "LIVE", revision: int = 1) -> dict[str, Any]: - return { - "schema": "dumbmoney.desired-mode.v1", - "venue": "dopey_robinhood", - "mode": mode, - "revision": revision, - "reason": "feed test desired mode", - "policy_epoch": 1, - "not_before": _format_utc(NOW - timedelta(seconds=5)), - "expires_at": _format_utc(NOW + timedelta(minutes=10)), - } - - -def _capital_body(generation: int = 1) -> dict[str, Any]: - return { - "schema": "dumbmoney.capital-envelope.v1", - "envelope_id": hashlib.sha256(f"capital-{generation}".encode()).hexdigest(), - "mandate_id": "9" * 64, - "venue": "dopey_robinhood", - "account_hash": ACCOUNT_HASH, - "strategy_hashes": [STRATEGY_HASH], - "passport_hashes": [PASSPORT_HASH], - "promotion_hashes": [PROMOTION_HASH], - "authorized_instruments": ["equity:ACME"], - "authorized_mode": "LIVE", - "max_order_risk_cents": 100, - "max_open_risk_cents": 500, - "max_correlated_risk_cents": 300, - "max_daily_loss_cents": 300, - "max_open_orders": 1, - "fencing_generation": generation, - "policy_epoch": 1, - "not_before": _format_utc(NOW), - "expires_at": _format_utc(NOW + timedelta(seconds=60)), - } - - -def _command( - envelope: dict[str, Any], - global_sequence: int, - *, - valid_now: bool = True, -) -> dict[str, Any]: - body = envelope["body"] - positive_grant = ( - body["schema"] == "dumbmoney.capital-envelope.v1" - or (body["schema"] == "dumbmoney.desired-mode.v1" and body["mode"] == "LIVE") - or (body["schema"] == "dumbmoney.kill-state.v1" and body["active"] is False) - ) - return { - "global_sequence": global_sequence, - "event_id": envelope["event_id"], - "body_schema": envelope["body_schema"], - "valid_now": valid_now, - "transport_window_current": valid_now, - "authority_effect": ( - "APPLY_FAIL_CLOSED" - if not positive_grant - else "APPLY_POSITIVE" - if valid_now - else "HISTORICAL_ONLY" - ), - "envelope": envelope, - } - - -def _page( - commands: list[dict[str, Any]], - *, - checkpoint_key: Ed25519PrivateKey, - request_nonce: str = TEST_REQUEST_NONCE, - after_sequence: int = 0, - after_digest: str = ZERO_DIGEST, - next_sequence: int | None = None, - next_digest: str | None = None, - ledger_head_sequence: int | None = None, - required_action: str = "APPLY_SIGNED_CONTROLS", -) -> dict[str, Any]: - projected_commands: list[dict[str, Any]] = [] - previous_global_digest = after_digest - for command in commands: - envelope = command["envelope"] - proof: dict[str, Any] = { - "schema": "dumbmoney.ledger-event-proof.v1", - "global_sequence": command["global_sequence"], - "event_id": envelope["event_id"], - "source_id": envelope["source_id"], - "source_sequence": envelope["source_sequence"], - "signer_key_id": envelope["signer_key_id"], - "nonce": envelope["nonce"], - "event_schema": envelope["body_schema"], - "observed_at": _format_utc(NOW), - "received_at": _format_utc(NOW), - "correlation_id": envelope["correlation_id"], - "causation_id": envelope["causation_id"], - "payload_digest": envelope["body_digest"], - "previous_source_digest": ( - ZERO_DIGEST - if envelope["source_sequence"] == 1 - else hashlib.sha256( - ( - f"source-head-{envelope['source_id']}-" - f"{envelope['source_sequence'] - 1}" - ).encode() - ).hexdigest() - ), - "previous_global_digest": previous_global_digest, - } - event_core = {key: value for key, value in proof.items() if key != "schema"} - event_digest = hashlib.sha256(canonical_json_bytes(event_core)).hexdigest() - proof["event_digest"] = event_digest - projected = { - **command, - "event_digest": event_digest, - "ledger_proof": proof, - } - projected_commands.append(projected) - previous_global_digest = event_digest - resolved_next = ( - next_sequence - if next_sequence is not None - else ( - int(projected_commands[-1]["global_sequence"]) - if projected_commands - else after_sequence - ) - ) - resolved_digest = ( - next_digest - if next_digest is not None - else ( - str(projected_commands[-1]["event_digest"]) - if projected_commands - and projected_commands[-1]["global_sequence"] == resolved_next - else after_digest - ) - ) - head = ledger_head_sequence if ledger_head_sequence is not None else resolved_next - head_digest = ( - resolved_digest - if head == resolved_next - else hashlib.sha256(f"ledger-head-{head}".encode()).hexdigest() - ) - checkpoint = { - "schema": "dumbmoney.cell-command-checkpoint.v1", - "cell_id": "dopey_robinhood", - "request_nonce": request_nonce, - "after_sequence": after_sequence, - "after_digest": after_digest, - "ordered_commands": [ - {key: value for key, value in command.items() if key != "envelope"} - for command in projected_commands - ], - "next_sequence": resolved_next, - "next_digest": resolved_digest, - "ledger_head_sequence": head, - "ledger_head_digest": head_digest, - "observed_at": _format_utc(NOW), - "required_action": required_action, - } - signature = checkpoint_key.sign(canonical_json_bytes(checkpoint)) - checkpoint_public = checkpoint_key.public_key().public_bytes_raw() - return { - "schema": "dumbmoney.cell-command-page.v1", - "cell_id": "dopey_robinhood", - "request_nonce": request_nonce, - "observed_at": _format_utc(NOW), - "after_sequence": after_sequence, - "after_digest": after_digest, - "next_sequence": resolved_next, - "next_digest": resolved_digest, - "ledger_head_sequence": head, - "ledger_head_digest": head_digest, - "has_more": resolved_next < head, - "required_action": required_action, - "commands": projected_commands, - "checkpoint": checkpoint, - "checkpoint_signature": { - "algorithm": "Ed25519", - "signer_key_id": hashlib.sha256(checkpoint_public).hexdigest(), - "signature": base64.urlsafe_b64encode(signature) - .decode("ascii") - .rstrip("="), - }, - } - - -def _response(page: dict[str, Any], status: int = 200) -> CommandFeedHttpResponse: - return CommandFeedHttpResponse( - status=status, - headers={"Content-Type": "application/json; charset=utf-8"}, - body=json.dumps(page, sort_keys=True, separators=(",", ":")).encode(), - ) - - -def _resign_checkpoint( - page: dict[str, Any], - checkpoint_key: Ed25519PrivateKey, -) -> None: - signature = checkpoint_key.sign(canonical_json_bytes(page["checkpoint"])) - public_key = checkpoint_key.public_key().public_bytes_raw() - page["checkpoint_signature"] = { - "algorithm": "Ed25519", - "signer_key_id": hashlib.sha256(public_key).hexdigest(), - "signature": base64.urlsafe_b64encode(signature).decode("ascii").rstrip("="), - } - - -class QueueTransport: - def __init__( - self, - *responses: CommandFeedHttpResponse | BaseException, - ) -> None: - self.responses = list(responses) - self.requests: list[CommandFeedHttpRequest] = [] - - def __call__( - self, - request: CommandFeedHttpRequest, - ) -> CommandFeedHttpResponse: - self.requests.append(request) - response = self.responses.pop(0) - if isinstance(response, BaseException): - raise response - return response - - -def _poll( - tmp_path: Path, - transport: QueueTransport, - core_public_key: bytes, - operator_public_key: bytes, - *, - request_nonce: str = TEST_REQUEST_NONCE, -): - operator_key_id = hashlib.sha256(operator_public_key).hexdigest() - with patch( - "dopey_live.dumbmoney_command_feed.secrets.token_hex", - return_value=request_nonce, - ): - return poll_once( - base_url="http://127.0.0.1:8765", - cell_bearer_provider=( - lambda: "dopey-cell-token-with-adequate-entropy-123456" - ), - trusted_public_key=core_public_key, - trusted_operator_public_keys={ - operator_key_id: operator_public_key, - }, - expected_account_hash=ACCOUNT_HASH, - state_path=tmp_path / "command-state.json", - transport=transport, - now_utc=NOW, - ) - - -def test_mode_and_capital_dispatch_survive_restart_cursor(tmp_path: Path) -> None: - core_key = Ed25519PrivateKey.generate() - operator_key = Ed25519PrivateKey.generate() - core_public_key = core_key.public_key().public_bytes_raw() - operator_public_key = operator_key.public_key().public_bytes_raw() - kill = _sign( - operator_key, - _kill_body(False), - source_sequence=1, - not_before=NOW - timedelta(seconds=5), - expires_at=NOW + timedelta(minutes=10), - ) - mode = _sign( - operator_key, - _mode_body(), - source_sequence=2, - not_before=NOW - timedelta(seconds=5), - expires_at=NOW + timedelta(minutes=10), - ) - capital = _sign( - core_key, - _capital_body(), - source_sequence=3, - not_before=NOW, - expires_at=NOW + timedelta(seconds=60), - ) - commands = [ - _command(kill, 1), - _command(mode, 2), - _command(capital, 3), - ] - first_page = _page(commands, checkpoint_key=core_key) - first_transport = QueueTransport(_response(first_page)) - - first = _poll( - tmp_path, - first_transport, - core_public_key, - operator_public_key, - ) - - assert first.allow_new_risk is True - assert first.desired_mode == "LIVE" - assert first.kill_active is False - assert first.capital_authority is not None - assert ( - first.capital_authority.verify(NOW, ACCOUNT_HASH).event_id - == (capital["event_id"]) - ) - assert first.next_sequence == 3 - request = first_transport.requests[0] - assert request.method == "GET" - assert request.headers["Authorization"].startswith("Bearer ") - query = parse_qs(urlsplit(request.url).query) - assert query == { - "after": ["0"], - "cursor": [ZERO_DIGEST], - "limit": ["250"], - "request_nonce": [TEST_REQUEST_NONCE], - } - - second_page = _page( - [], - checkpoint_key=core_key, - after_sequence=3, - after_digest=first.next_digest, - next_sequence=3, - next_digest=first.next_digest, - ) - second_transport = QueueTransport(_response(second_page)) - restarted = _poll( - tmp_path, - second_transport, - core_public_key, - operator_public_key, - ) - - assert restarted.allow_new_risk is True - assert restarted.next_sequence == 3 - assert parse_qs(urlsplit(second_transport.requests[0].url).query)["after"] == ["3"] - - -@pytest.mark.parametrize( - "case", - ["checkpoint_signature", "ledger_proof"], -) -def test_checkpoint_and_ledger_proof_tamper_fail_closed( - tmp_path: Path, - case: str, -) -> None: - core_key = Ed25519PrivateKey.generate() - operator_key = Ed25519PrivateKey.generate() - core_public_key = core_key.public_key().public_bytes_raw() - operator_public_key = operator_key.public_key().public_bytes_raw() - kill = _sign( - operator_key, - _kill_body(True), - source_sequence=1, - not_before=NOW - timedelta(seconds=5), - expires_at=NOW + timedelta(minutes=10), - ) - page = _page( - [_command(kill, 1)], - checkpoint_key=core_key, - required_action="CANCEL_AND_RECONCILE", - ) - if case == "checkpoint_signature": - page["checkpoint"]["required_action"] = "PAUSE_NEW_RISK" - else: - page["commands"][0]["ledger_proof"]["payload_digest"] = "e" * 64 - page["checkpoint"]["ordered_commands"][0]["ledger_proof"]["payload_digest"] = ( - "e" * 64 - ) - _resign_checkpoint(page, core_key) - - result = _poll( - tmp_path, - QueueTransport(_response(page)), - core_public_key, - operator_public_key, - ) - - assert result.allow_new_risk is False - assert result.kill_active is True - assert result.next_sequence == 0 - assert not (tmp_path / "command-state.json").exists() - - -def test_replayed_signed_page_under_different_request_nonce_fails_closed( - tmp_path: Path, -) -> None: - core_key = Ed25519PrivateKey.generate() - operator_key = Ed25519PrivateKey.generate() - replayed_page = _page( - [], - checkpoint_key=core_key, - request_nonce=TEST_REQUEST_NONCE, - required_action="CANCEL_AND_RECONCILE", - ) - transport = QueueTransport(_response(replayed_page)) - - result = _poll( - tmp_path, - transport, - core_key.public_key().public_bytes_raw(), - operator_key.public_key().public_bytes_raw(), - request_nonce="8" * 64, - ) - - assert result.allow_new_risk is False - assert result.kill_active is True - assert result.next_sequence == 0 - assert parse_qs(urlsplit(transport.requests[0].url).query)["request_nonce"] == [ - "8" * 64 - ] - assert not (tmp_path / "command-state.json").exists() - - -@pytest.mark.parametrize("case", ["operator_control_by_core", "capital_by_operator"]) -def test_command_schema_signer_role_mismatch_fails_closed( - tmp_path: Path, - case: str, -) -> None: - core_key = Ed25519PrivateKey.generate() - operator_key = Ed25519PrivateKey.generate() - if case == "operator_control_by_core": - envelope = _sign( - core_key, - _kill_body(True), - source_sequence=1, - not_before=NOW - timedelta(seconds=5), - expires_at=NOW + timedelta(minutes=10), - ) - required_action = "CANCEL_AND_RECONCILE" - else: - envelope = _sign( - operator_key, - _capital_body(), - source_sequence=1, - not_before=NOW, - expires_at=NOW + timedelta(seconds=60), - ) - required_action = "PAUSE_NEW_RISK" - page = _page( - [_command(envelope, 1)], - checkpoint_key=core_key, - required_action=required_action, - ) - - result = _poll( - tmp_path, - QueueTransport(_response(page)), - core_key.public_key().public_bytes_raw(), - operator_key.public_key().public_bytes_raw(), - ) - - assert result.allow_new_risk is False - assert result.kill_active is True - assert result.next_sequence == 0 - assert not (tmp_path / "command-state.json").exists() - - -class KillGateway: - def __init__(self, open_account: AccountSnapshot) -> None: - self.account = open_account - self.cancel_calls: list[tuple[str, str]] = [] - - def get_account_snapshot(self) -> AccountSnapshot: - return self.account - - def cancel_order( - self, - instrument: str, - order_id: str, - ) -> GatewayCancelResult: - self.cancel_calls.append((instrument, order_id)) - canceled_order = { - **self.account.recent_equity_orders[0], - "status": "canceled", - } - self.account = AccountSnapshot( - **{ - **self.account.to_dict(), - "recent_equity_orders": (canceled_order,), - } - ) - return GatewayCancelResult( - cancel_requested=True, - order_id=order_id, - status="cancel_requested", - warnings=(), - error="", - observed_tools=("cancel_equity_order",), - ) - - -def _account_with_open_order() -> AccountSnapshot: - return AccountSnapshot( - authenticated=True, - account_type="individual", - buying_power_usd=100.0, - total_equity_usd=100.0, - equity_positions=(), - option_positions=(), - recent_equity_orders=( - { - "order_id": "order-1", - "symbol": "ACME", - "side": "buy", - "status": "open", - "market_date": "2099-01-01", - "submitted_at": "2099-01-01T00:00:00Z", - "filled_at": "", - "filled_quantity": 0.0, - "average_fill_price": 0.0, - }, - ), - recent_option_orders=(), - equity_order_history_complete=True, - option_order_history_complete=True, - error="", - account_hash=ACCOUNT_HASH, - ) - - -def _entry_intent() -> LiveExecutionIntent: - return LiveExecutionIntent( - intent_id="feed-race-entry", - instrument="equity", - symbol="ACME", - side="buy", - order_type="limit", - limit_price=100.0, - time_in_force="day", - extended_hours=False, - notional_usd=1.0, - quantity=0.01, - source_as_of_date="2098-12-31", - source_hash=STRATEGY_HASH, - thesis_direction="bullish", - analysis_symbol="ACME", - analysis_relationship="self", - analysis_reference_price=100.0, - analysis_invalidation_price=90.0, - analysis_target_price=120.0, - daily_leverage_multiple=1.0, - daily_reset=False, - maximum_holding_sessions=5, - reference_price=100.0, - invalidation_price=90.0, - target_price=120.0, - expected_hold_trading_days=5, - execution_snapshot=None, - option_contract={}, - reason_codes=("feed-race-fixture",), - promotion_hash=PROMOTION_HASH, - passport_hash=PASSPORT_HASH, - ) - - -class RevocationRaceGateway: - def __init__(self, account: AccountSnapshot) -> None: - self.account = account - self.placement_calls = 0 - self.guard_calls = 0 - - def get_account_snapshot(self) -> AccountSnapshot: - return self.account - - def get_equity_execution_snapshot( - self, - _intent: LiveExecutionIntent, - ) -> object: - return object() - - def place_order( - self, - _intent: LiveExecutionIntent, - _paper_twin_record_hash: str, - *, - pre_submit_guard: Callable[[], datetime], - ) -> GatewayOrderResult: - pre_submit_guard() - self.guard_calls += 1 - self.placement_calls += 1 - return GatewayOrderResult( - review_succeeded=True, - placed=True, - order_id="broker-order-race-1", - status="queued", - filled_quantity=0.0, - average_fill_price=0.0, - submitted_at=_format_utc(NOW), - filled_at="", - warnings=(), - error="", - observed_tools=("review_equity_order", "place_equity_order"), - ) - - -class StaticPromotionResolver: - def bind_intent( - self, - authority: VerifiedCapitalAuthority, - intent: LiveExecutionIntent, - _now_utc: datetime, - *, - expected_binding: VerifiedPromotionBinding | None = None, - ) -> tuple[LiveExecutionIntent, VerifiedPromotionBinding]: - binding = VerifiedPromotionBinding( - strategy_hash=STRATEGY_HASH, - passport_hash=PASSPORT_HASH, - promotion_hash=PROMOTION_HASH, - instrument_id="equity:ACME", - maximum_loss_cents=100, - policy_epoch=authority.envelope.policy_epoch, - capital_envelope_id=authority.envelope.envelope_id, - capital_event_id=authority.event_id, - capital_body_digest=authority.body_digest, - capital_fencing_generation=authority.envelope.fencing_generation, - passport_event_id="4" * 64, - promotion_event_id="5" * 64, - passport_signer_key_id="6" * 64, - promotion_signer_key_id="7" * 64, - expires_at=authority.envelope.expires_at, - ) - if expected_binding is not None: - assert binding == expected_binding - return intent, binding - - -def test_kill_arriving_during_atomic_write_is_ambiguous_and_never_retried( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - core_key = Ed25519PrivateKey.generate() - operator_key = Ed25519PrivateKey.generate() - core_public_key = core_key.public_key().public_bytes_raw() - operator_public_key = operator_key.public_key().public_bytes_raw() - initial_commands = [ - _command( - _sign( - operator_key, - _kill_body(False), - source_sequence=1, - not_before=NOW - timedelta(seconds=5), - expires_at=NOW + timedelta(minutes=10), - ), - 1, - ), - _command( - _sign( - operator_key, - _mode_body(), - source_sequence=2, - not_before=NOW - timedelta(seconds=5), - expires_at=NOW + timedelta(minutes=10), - ), - 2, - ), - _command( - _sign( - core_key, - _capital_body(), - source_sequence=3, - not_before=NOW, - expires_at=NOW + timedelta(seconds=60), - ), - 3, - ), - ] - initial = _poll( - tmp_path, - QueueTransport(_response(_page(initial_commands, checkpoint_key=core_key))), - core_public_key, - operator_public_key, - ) - assert initial.allow_new_risk is True - - kill_during_write = _sign( - operator_key, - _kill_body(True, generation=2), - source_sequence=4, - not_before=NOW - timedelta(seconds=5), - expires_at=NOW + timedelta(minutes=10), - ) - refresh_transport = QueueTransport( - _response( - _page( - [], - checkpoint_key=core_key, - after_sequence=3, - after_digest=initial.next_digest, - next_sequence=3, - next_digest=initial.next_digest, - ) - ), - _response( - _page( - [_command(kill_during_write, 4)], - checkpoint_key=core_key, - after_sequence=3, - after_digest=initial.next_digest, - required_action="CANCEL_AND_RECONCILE", - ) - ), - ) - - def refresh(_now_utc: datetime): - return _poll( - tmp_path, - refresh_transport, - core_public_key, - operator_public_key, - ) - - account = AccountSnapshot( - authenticated=True, - account_type="individual", - buying_power_usd=100.0, - total_equity_usd=100.0, - equity_positions=(), - option_positions=(), - recent_equity_orders=(), - recent_option_orders=(), - equity_order_history_complete=True, - option_order_history_complete=True, - error="", - account_hash=ACCOUNT_HASH, - ) - gateway = RevocationRaceGateway(account) - intent = _entry_intent() - monkeypatch.setattr( - live_service, - "load_live_branch_control", - lambda: SimpleNamespace( - live_branch_enabled=True, - stock_execution_enabled=True, - options_execution_enabled=False, - use_full_available_balance=False, - ), - ) - monkeypatch.setattr( - live_service, - "_robinhood_stock_submission_state", - lambda _now: (True, "2099-01-01", True), - ) - monkeypatch.setattr( - live_service, - "research_maturity_blockers", - lambda _root: (), - ) - monkeypatch.setattr( - live_service, - "build_entry_intents", - lambda *_args, **_kwargs: (intent,), - ) - monkeypatch.setattr( - live_service, - "prepare_equity_entry_for_execution", - lambda *_args, **_kwargs: intent, - ) - - missing_refresh = run_live_branch_cycle( - tmp_path, - gateway, # type: ignore[arg-type] - NOW, - True, - dumbmoney_controls=initial, - authority_clock=lambda: NOW, - ) - assert missing_refresh["new_exposure_allowed"] is False - assert missing_refresh["cancel_only_reason"] == ( - "dumbmoney_command_feed_refresh_missing_at_broker_sink" - ) - assert gateway.placement_calls == 0 - - result = run_live_branch_cycle( - tmp_path, - gateway, # type: ignore[arg-type] - NOW, - True, - dumbmoney_controls=initial, - dumbmoney_control_refresh=refresh, - dumbmoney_promotion_resolver=StaticPromotionResolver(), - authority_clock=lambda: NOW, - ) - - assert gateway.guard_calls == 1 - assert gateway.placement_calls == 1 - assert len(refresh_transport.requests) == 2 - assert result["orders"][0]["status"] == ("ambiguous_in_flight_authority_revoked") - assert result["orders"][0]["placement_retried"] is False - event_types = [ - str(record["event_type"]) for record in read_ledger(ledger_path(tmp_path)) - ] - assert event_types.index("dumbmoney_order_dispatching") < event_types.index( - "live_order_submission_ambiguous" - ) - assert "live_order_result" not in event_types - - -def test_signed_kill_delivery_enters_existing_cancel_only_path( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - core_key = Ed25519PrivateKey.generate() - operator_key = Ed25519PrivateKey.generate() - core_public_key = core_key.public_key().public_bytes_raw() - operator_public_key = operator_key.public_key().public_bytes_raw() - kill = _sign( - operator_key, - _kill_body(True), - source_sequence=1, - not_before=NOW - timedelta(seconds=5), - expires_at=NOW + timedelta(minutes=10), - ) - dispatch = _poll( - tmp_path, - QueueTransport( - _response( - _page( - [_command(kill, 1)], - checkpoint_key=core_key, - required_action="CANCEL_AND_RECONCILE", - ) - ) - ), - core_public_key, - operator_public_key, - ) - assert dispatch.kill_active is True - append_record( - ledger_path(tmp_path), - "live_order_result", - { - "intent_id": "intent-1", - "instrument": "equity", - "placed": True, - "order_id": "order-1", - "status": "open", - }, - ) - monkeypatch.setattr( - live_service, - "load_live_branch_control", - lambda: SimpleNamespace(live_branch_enabled=False), - ) - monkeypatch.setattr( - live_service, - "_robinhood_stock_submission_state", - lambda _now: (True, "2099-01-01", True), - ) - gateway = KillGateway(_account_with_open_order()) - - result = run_live_branch_cycle( - tmp_path, - gateway, # type: ignore[arg-type] - NOW, - True, - dumbmoney_controls=dispatch, - ) - - assert result["authority_state"] == "CANCEL_ONLY_DEGRADED" - assert result["kill_active"] is True - assert gateway.cancel_calls == [("equity", "order-1")] - assert result["new_exposure_allowed"] is False - - -@pytest.mark.parametrize("case", ["tamper", "generation_gap"]) -def test_tamper_and_generation_gap_do_not_advance_cursor( - tmp_path: Path, - case: str, -) -> None: - core_key = Ed25519PrivateKey.generate() - operator_key = Ed25519PrivateKey.generate() - core_public_key = core_key.public_key().public_bytes_raw() - operator_public_key = operator_key.public_key().public_bytes_raw() - generation = 2 if case == "generation_gap" else 1 - kill = _sign( - operator_key, - _kill_body(True, generation), - source_sequence=1, - not_before=NOW - timedelta(seconds=5), - expires_at=NOW + timedelta(minutes=10), - ) - if case == "tamper": - kill["body"]["reason"] = "tampered" - - result = _poll( - tmp_path, - QueueTransport( - _response( - _page( - [_command(kill, 1)], - checkpoint_key=core_key, - ) - ) - ), - core_public_key, - operator_public_key, - ) - - assert result.allow_new_risk is False - assert result.kill_active is True - assert result.next_sequence == 0 - assert not (tmp_path / "command-state.json").exists() - - -@pytest.mark.parametrize( - ("response", "reason"), - [ - ( - CommandFeedHttpResponse( - status=401, - headers={"Content-Type": "application/json"}, - body=b"{}", - ), - "authentication", - ), - (TimeoutError("fixture transport timeout"), "transport"), - ], -) -def test_auth_and_transport_failure_are_fail_closed_unknown_kill( - tmp_path: Path, - response: CommandFeedHttpResponse | BaseException, - reason: str, -) -> None: - core_key = Ed25519PrivateKey.generate() - operator_key = Ed25519PrivateKey.generate() - result = _poll( - tmp_path, - QueueTransport(response), - core_key.public_key().public_bytes_raw(), - operator_key.public_key().public_bytes_raw(), - ) - - assert result.allow_new_risk is False - assert result.kill_active is True - assert reason in result.fail_closed_reason - assert result.next_sequence == 0 - - -def test_expired_active_kill_bootstrap_advances_and_remains_active( - tmp_path: Path, -) -> None: - core_key = Ed25519PrivateKey.generate() - operator_key = Ed25519PrivateKey.generate() - core_public_key = core_key.public_key().public_bytes_raw() - operator_public_key = operator_key.public_key().public_bytes_raw() - kill = _sign( - operator_key, - _kill_body(True), - source_sequence=1, - not_before=NOW - timedelta(minutes=10), - expires_at=NOW - timedelta(minutes=5), - ) - first_page = _page( - [_command(kill, 1, valid_now=False)], - checkpoint_key=core_key, - required_action="CANCEL_AND_RECONCILE", - ) - - first = _poll( - tmp_path, - QueueTransport(_response(first_page)), - core_public_key, - operator_public_key, - ) - - assert first.next_sequence == 1 - assert first.kill_active is True - assert first.allow_new_risk is False - persisted = json.loads( - (tmp_path / "command-state.json").read_text(encoding="utf-8") - ) - assert persisted["next_sequence"] == 1 - assert persisted["kill_envelope"]["event_id"] == kill["event_id"] - - restart_page = _page( - [], - checkpoint_key=core_key, - after_sequence=1, - after_digest=first.next_digest, - next_sequence=1, - next_digest=first.next_digest, - required_action="CANCEL_AND_RECONCILE", - ) - restarted = _poll( - tmp_path, - QueueTransport(_response(restart_page)), - core_public_key, - operator_public_key, - ) - assert restarted.kill_active is True - assert restarted.next_sequence == 1 - - -def test_page_omission_or_cursor_mismatch_never_reuses_positive_authority( - tmp_path: Path, -) -> None: - core_key = Ed25519PrivateKey.generate() - operator_key = Ed25519PrivateKey.generate() - core_public_key = core_key.public_key().public_bytes_raw() - operator_public_key = operator_key.public_key().public_bytes_raw() - mismatched = _page( - [], - checkpoint_key=core_key, - after_sequence=1, - after_digest="1" * 64, - next_sequence=1, - next_digest="1" * 64, - ) - - result = _poll( - tmp_path, - QueueTransport(_response(mismatched)), - core_public_key, - operator_public_key, - ) - - assert result.allow_new_risk is False - assert result.kill_active is True - assert result.next_sequence == 0 diff --git a/tests/test_dumbmoney_contract_feed.py b/tests/test_dumbmoney_contract_feed.py deleted file mode 100644 index 2549df0..0000000 --- a/tests/test_dumbmoney_contract_feed.py +++ /dev/null @@ -1,592 +0,0 @@ -from __future__ import annotations - -import base64 -import hashlib -import json -from dataclasses import replace -from datetime import datetime, timedelta, timezone -from typing import Any -from urllib.parse import parse_qs, urlsplit - -import pytest -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -from dopey_live.dumbmoney_authority import ( - VerifiedCapitalAuthority, - canonical_json_bytes, - verify_signed_capital_envelope, -) -from dopey_live.dumbmoney_command_feed import ( - CommandFeedHttpRequest, - CommandFeedHttpResponse, -) -from dopey_live.dumbmoney_contract_feed import ( - CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA, - CONTRACT_RESOLUTION_SCHEMA, - DumbMoneyContractResolverClient, -) -from dopey_live.dumbmoney_promotion import ( - ALPHA_PASSPORT_SCHEMA, - PROMOTION_CERTIFICATE_SCHEMA, - DumbMoneyPromotionError, -) -from dopey_live.models import LiveExecutionIntent - - -NOW = datetime(2099, 1, 1, 0, 0, 30, tzinfo=timezone.utc) -ACCOUNT_HASH = "c" * 64 -STRATEGY_HASH = "a" * 64 -ZERO_DIGEST = "0" * 64 - - -def _format_utc(value: datetime) -> str: - rendered = value.astimezone(timezone.utc).isoformat( - timespec="microseconds" if value.microsecond else "seconds" - ) - return rendered.replace("+00:00", "Z") - - -def _sign_envelope( - private_key: Ed25519PrivateKey, - body: dict[str, Any], - *, - source_id: str, - source_sequence: int, - not_before: datetime, - expires_at: datetime, -) -> dict[str, Any]: - public_key = private_key.public_key().public_bytes_raw() - unsigned: dict[str, Any] = { - "schema": "dumbmoney.signed-envelope.v1", - "source_id": source_id, - "source_sequence": source_sequence, - "correlation_id": "promotion-fixture", - "causation_id": None, - "nonce": f"{source_id}-nonce-{source_sequence}", - "not_before": _format_utc(not_before), - "expires_at": _format_utc(expires_at), - "body_schema": body["schema"], - "body_digest": hashlib.sha256(canonical_json_bytes(body)).hexdigest(), - "body": body, - "signature_algorithm": "Ed25519", - "signer_key_id": hashlib.sha256(public_key).hexdigest(), - } - unsigned["event_id"] = hashlib.sha256(canonical_json_bytes(unsigned)).hexdigest() - signature = private_key.sign(canonical_json_bytes(unsigned)) - return { - **unsigned, - "signature": base64.urlsafe_b64encode(signature).decode("ascii").rstrip("="), - } - - -def _passport_body() -> dict[str, Any]: - return { - "schema": ALPHA_PASSPORT_SCHEMA, - "passport_id": "passport-dopey-1", - "strategy_lineage_id": "lineage-dopey-1", - "venue": "dopey_robinhood", - "strategy_hash": STRATEGY_HASH, - "artifact_hashes": ["1" * 64], - "evidence_verdict_hashes": ["2" * 64], - "intended_instruments": ["equity:ACME"], - "maximum_loss_cents": 100, - "evidence_class": "FORWARD", - "created_at": _format_utc(NOW - timedelta(days=1)), - "expires_at": _format_utc(NOW + timedelta(days=1)), - } - - -def _promotion_body( - passport_digest: str, - *, - stage: str = "EXPLORATORY_LIVE", -) -> dict[str, Any]: - return { - "schema": PROMOTION_CERTIFICATE_SCHEMA, - "certificate_id": "promotion-dopey-1", - "passport_digest": passport_digest, - "verdict_digests": ["3" * 64], - "stage": stage, - "venue": "dopey_robinhood", - "instruments": ["equity:ACME"], - "maximum_loss_cents": 100, - "rollback_triggers": ["AMBIGUOUS_OUTCOME", "KILL_ACTIVE"], - "policy_epoch": 1, - "not_before": _format_utc(NOW - timedelta(days=1)), - "expires_at": _format_utc(NOW + timedelta(days=1)), - } - - -def _capital_body( - passport_digest: str, - promotion_digest: str, -) -> dict[str, Any]: - return { - "schema": "dumbmoney.capital-envelope.v1", - "envelope_id": "e" * 64, - "mandate_id": "d" * 64, - "venue": "dopey_robinhood", - "account_hash": ACCOUNT_HASH, - "strategy_hashes": [STRATEGY_HASH], - "passport_hashes": [passport_digest], - "promotion_hashes": [promotion_digest], - "authorized_instruments": ["equity:ACME"], - "authorized_mode": "LIVE", - "max_order_risk_cents": 100, - "max_open_risk_cents": 1250, - "max_correlated_risk_cents": 300, - "max_daily_loss_cents": 300, - "max_open_orders": 5, - "fencing_generation": 7, - "policy_epoch": 1, - "not_before": _format_utc(NOW), - "expires_at": _format_utc(NOW + timedelta(seconds=60)), - } - - -def _intent() -> LiveExecutionIntent: - return LiveExecutionIntent( - intent_id="intent-1", - instrument="equity", - symbol="ACME", - side="buy", - order_type="limit", - limit_price=100.0, - time_in_force="day", - extended_hours=False, - notional_usd=1.0, - quantity=0.01, - source_as_of_date="2098-12-31", - source_hash=STRATEGY_HASH, - thesis_direction="bullish", - analysis_symbol="ACME", - analysis_relationship="self", - analysis_reference_price=100.0, - analysis_invalidation_price=90.0, - analysis_target_price=120.0, - daily_leverage_multiple=1.0, - daily_reset=False, - maximum_holding_sessions=5, - reference_price=100.0, - invalidation_price=90.0, - target_price=120.0, - expected_hold_trading_days=5, - execution_snapshot=None, - option_contract={}, - reason_codes=("resolver-fixture",), - ) - - -def _ledger_proof( - envelope: dict[str, Any], - *, - global_sequence: int, -) -> dict[str, Any]: - received_at = NOW - timedelta(seconds=1) - observed_at = max( - received_at, - datetime.fromisoformat(envelope["not_before"].replace("Z", "+00:00")), - ) - proof: dict[str, Any] = { - "schema": "dumbmoney.ledger-event-proof.v1", - "global_sequence": global_sequence, - "event_id": envelope["event_id"], - "source_id": envelope["source_id"], - "source_sequence": envelope["source_sequence"], - "signer_key_id": envelope["signer_key_id"], - "nonce": envelope["nonce"], - "event_schema": envelope["body_schema"], - "observed_at": _format_utc(observed_at), - "received_at": _format_utc(received_at), - "correlation_id": envelope["correlation_id"], - "causation_id": envelope["causation_id"], - "payload_digest": envelope["body_digest"], - "previous_source_digest": ZERO_DIGEST, - "previous_global_digest": (ZERO_DIGEST if global_sequence == 1 else "9" * 64), - } - proof["event_digest"] = hashlib.sha256( - canonical_json_bytes( - {key: value for key, value in proof.items() if key != "schema"} - ) - ).hexdigest() - return proof - - -def _resolution( - core_key: Ed25519PrivateKey, - envelope: dict[str, Any], - request_nonce: str, - capital_digest: str, - authority_state: dict[str, Any], - *, - global_sequence: int, - eligible: bool = True, -) -> dict[str, Any]: - proof = _ledger_proof(envelope, global_sequence=global_sequence) - checkpoint: dict[str, Any] = { - "schema": CONTRACT_RESOLUTION_CHECKPOINT_SCHEMA, - "cell_id": "dopey_robinhood", - "request_nonce": request_nonce, - "requested_body_digest": envelope["body_digest"], - "capital_envelope_digest": capital_digest, - "fencing_generation": 7, - "observed_at": _format_utc(NOW), - "contract_schema": envelope["body_schema"], - "transport_window_current": True, - "body_window_current": True, - "eligible_live_input": eligible, - "authority_state": authority_state, - "ledger_proof": proof, - "envelope": envelope, - } - public_key = core_key.public_key().public_bytes_raw() - signature = core_key.sign(canonical_json_bytes(checkpoint)) - return { - "schema": CONTRACT_RESOLUTION_SCHEMA, - **{key: value for key, value in checkpoint.items() if key != "schema"}, - "checkpoint": checkpoint, - "checkpoint_signature": { - "algorithm": "Ed25519", - "signer_key_id": hashlib.sha256(public_key).hexdigest(), - "signature": base64.urlsafe_b64encode(signature) - .decode("ascii") - .rstrip("="), - }, - } - - -def _resign_resolution( - response: dict[str, Any], - core_key: Ed25519PrivateKey, -) -> dict[str, Any]: - signature = core_key.sign(canonical_json_bytes(response["checkpoint"])) - response["checkpoint_signature"]["signature"] = ( - base64.urlsafe_b64encode(signature).decode("ascii").rstrip("=") - ) - return response - - -def _authority_state( - authority: VerifiedCapitalAuthority, - *, - passport_event_digest: str, - promotion_event_digest: str, - evaluator_key_ids: tuple[str, str], -) -> dict[str, Any]: - return { - "schema": "dumbmoney.cell-authority-state.v1", - "evaluated_at": _format_utc(NOW), - "authority_valid_until": _format_utc(NOW + timedelta(seconds=30)), - "policy_epoch": authority.envelope.policy_epoch, - "mandate_id": authority.envelope.mandate_id, - "mandate_event_digest": "4" * 64, - "kill_clear": True, - "kill_generation": 1, - "kill_event_digest": "5" * 64, - "desired_mode": "LIVE", - "desired_mode_revision": 1, - "desired_mode_event_digest": "6" * 64, - "capital_envelope_digest": authority.body_digest, - "capital_event_digest": "7" * 64, - "fencing_generation": authority.envelope.fencing_generation, - "strategy_hash": authority.envelope.strategy_hashes[0], - "passport_digest": authority.envelope.passport_hashes[0], - "passport_event_digest": passport_event_digest, - "promotion_digest": authority.envelope.promotion_hashes[0], - "promotion_event_digest": promotion_event_digest, - "verdicts": [ - { - "verdict_digest": "8" * 64, - "verdict_event_digest": "a" * 64, - "verdict_id": "verdict-integrity-1", - "court": "integrity", - "decision": "PASS", - "signer_key_id": evaluator_key_ids[0], - "evaluated_at": _format_utc(NOW - timedelta(seconds=5)), - "expires_at": _format_utc(NOW + timedelta(seconds=45)), - "transport_expires_at": _format_utc(NOW + timedelta(seconds=45)), - }, - { - "verdict_digest": "b" * 64, - "verdict_event_digest": "c" * 64, - "verdict_id": "verdict-statistics-1", - "court": "statistics", - "decision": "PASS", - "signer_key_id": evaluator_key_ids[1], - "evaluated_at": _format_utc(NOW - timedelta(seconds=5)), - "expires_at": _format_utc(NOW + timedelta(seconds=45)), - "transport_expires_at": _format_utc(NOW + timedelta(seconds=45)), - }, - ], - "ledger_head_sequence": 2, - "ledger_head_digest": promotion_event_digest, - } - - -class ResolutionTransport: - def __init__( - self, - core_key: Ed25519PrivateKey, - authority: VerifiedCapitalAuthority, - envelopes: dict[str, dict[str, Any]], - evaluator_key_ids: tuple[str, str], - *, - response_mutator: Any = None, - ) -> None: - self.core_key = core_key - self.authority = authority - self.envelopes = envelopes - self.evaluator_key_ids = evaluator_key_ids - self.response_mutator = response_mutator - self.requests: list[CommandFeedHttpRequest] = [] - - def __call__(self, request: CommandFeedHttpRequest) -> CommandFeedHttpResponse: - self.requests.append(request) - parsed = urlsplit(request.url) - digest = parsed.path.rsplit("/", 1)[-1] - query = parse_qs(parsed.query) - assert query["capital_envelope_digest"] == [self.authority.body_digest] - nonce = query["request_nonce"][0] - envelope = self.envelopes[digest] - passport_envelope = self.envelopes[self.authority.envelope.passport_hashes[0]] - promotion_envelope = self.envelopes[self.authority.envelope.promotion_hashes[0]] - authority_state = _authority_state( - self.authority, - passport_event_digest=_ledger_proof( - passport_envelope, - global_sequence=1, - )["event_digest"], - promotion_event_digest=_ledger_proof( - promotion_envelope, - global_sequence=2, - )["event_digest"], - evaluator_key_ids=self.evaluator_key_ids, - ) - response = _resolution( - self.core_key, - envelope, - nonce, - self.authority.body_digest, - authority_state, - global_sequence=( - 1 if envelope["body_schema"] == ALPHA_PASSPORT_SCHEMA else 2 - ), - ) - if self.response_mutator is not None: - response = self.response_mutator(response) - response = _resign_resolution(response, self.core_key) - return CommandFeedHttpResponse( - status=200, - headers={"Content-Type": "application/json"}, - body=json.dumps(response, separators=(",", ":")).encode("utf-8"), - ) - - -def _runtime( - *, - promotion_signer: str = "promoter", - stage: str = "EXPLORATORY_LIVE", - response_mutator: Any = None, -) -> tuple[ - DumbMoneyContractResolverClient, - VerifiedCapitalAuthority, - ResolutionTransport, -]: - core_key = Ed25519PrivateKey.generate() - promoter_key = Ed25519PrivateKey.generate() - research_key = Ed25519PrivateKey.generate() - evaluator_key_1 = Ed25519PrivateKey.generate() - evaluator_key_2 = Ed25519PrivateKey.generate() - passport = _sign_envelope( - research_key, - _passport_body(), - source_id="research", - source_sequence=1, - not_before=NOW - timedelta(seconds=10), - expires_at=NOW + timedelta(minutes=10), - ) - signing_key = research_key if promotion_signer == "research" else promoter_key - promotion = _sign_envelope( - signing_key, - _promotion_body(passport["body_digest"], stage=stage), - source_id="promoter", - source_sequence=1, - not_before=NOW - timedelta(seconds=10), - expires_at=NOW + timedelta(minutes=10), - ) - capital = _sign_envelope( - core_key, - _capital_body(passport["body_digest"], promotion["body_digest"]), - source_id="dumbmoney-core", - source_sequence=1, - not_before=NOW, - expires_at=NOW + timedelta(seconds=60), - ) - core_public = core_key.public_key().public_bytes_raw() - authority = verify_signed_capital_envelope( - capital, - core_public, - NOW, - ACCOUNT_HASH, - ) - transport = ResolutionTransport( - core_key, - authority, - { - passport["body_digest"]: passport, - promotion["body_digest"]: promotion, - }, - ( - hashlib.sha256(evaluator_key_1.public_key().public_bytes_raw()).hexdigest(), - hashlib.sha256(evaluator_key_2.public_key().public_bytes_raw()).hexdigest(), - ), - response_mutator=response_mutator, - ) - promoter_public = promoter_key.public_key().public_bytes_raw() - research_public = research_key.public_key().public_bytes_raw() - evaluator_public_1 = evaluator_key_1.public_key().public_bytes_raw() - evaluator_public_2 = evaluator_key_2.public_key().public_bytes_raw() - client = DumbMoneyContractResolverClient( - base_url="http://127.0.0.1:8765", - cell_bearer_provider=lambda: "cell-token-from-credential-manager", - trusted_core_public_key=core_public, - trusted_promoter_public_keys={ - hashlib.sha256(promoter_public).hexdigest(): promoter_public, - }, - trusted_research_public_keys={ - hashlib.sha256(research_public).hexdigest(): research_public, - }, - trusted_evaluator_public_keys={ - hashlib.sha256(evaluator_public_1).hexdigest(): evaluator_public_1, - hashlib.sha256(evaluator_public_2).hexdigest(): evaluator_public_2, - }, - transport=transport, - ) - return client, authority, transport - - -def test_nonce_and_fence_bound_resolver_hydrates_exact_lineage() -> None: - client, authority, transport = _runtime() - - intent, binding = client.bind_intent(authority, _intent(), NOW) - - assert intent.passport_hash == authority.envelope.passport_hashes[0] - assert intent.promotion_hash == authority.envelope.promotion_hashes[0] - assert binding.strategy_hash == STRATEGY_HASH - assert binding.capital_body_digest == authority.body_digest - assert binding.capital_fencing_generation == 7 - assert len(transport.requests) == 2 - nonces = [ - parse_qs(urlsplit(request.url).query)["request_nonce"][0] - for request in transport.requests - ] - assert len(set(nonces)) == 2 - assert all(len(nonce) == 64 for nonce in nonces) - assert all( - request.headers["Authorization"] == "Bearer cell-token-from-credential-manager" - for request in transport.requests - ) - - -def test_resolver_rejects_caller_supplied_or_wrong_role_lineage() -> None: - client, authority, _ = _runtime() - with pytest.raises(DumbMoneyPromotionError, match="supplied its own"): - client.bind_intent( - authority, - replace(_intent(), promotion_hash=authority.envelope.promotion_hashes[0]), - NOW, - ) - - wrong_role_client, wrong_role_authority, _ = _runtime(promotion_signer="research") - with pytest.raises(DumbMoneyPromotionError, match="sealed promoter"): - wrong_role_client.bind_intent(wrong_role_authority, _intent(), NOW) - - -def test_resolver_rejects_nonlive_stage_even_if_projection_lies() -> None: - client, authority, _ = _runtime(stage="PAPER") - - with pytest.raises( - DumbMoneyPromotionError, - match="stage is not live-authorizing|live-eligibility projection", - ): - client.bind_intent(authority, _intent(), NOW) - - -def test_resolver_rejects_response_transplanted_across_capital_fence() -> None: - def mutate(response: dict[str, Any]) -> dict[str, Any]: - response["capital_envelope_digest"] = "8" * 64 - return response - - client, authority, _ = _runtime(response_mutator=mutate) - - with pytest.raises( - DumbMoneyPromotionError, - match="checkpoint does not bind|active capital envelope", - ): - client.bind_intent(authority, _intent(), NOW) - - -def test_resolver_rejects_expired_authority_state() -> None: - def mutate(response: dict[str, Any]) -> dict[str, Any]: - response["authority_state"]["authority_valid_until"] = _format_utc( - NOW - timedelta(seconds=1) - ) - return response - - client, authority, _ = _runtime(response_mutator=mutate) - - with pytest.raises( - DumbMoneyPromotionError, - match="authority_state is stale", - ): - client.bind_intent(authority, _intent(), NOW) - - -def test_resolver_rejects_unsealed_authority_verdict_signer() -> None: - def mutate(response: dict[str, Any]) -> dict[str, Any]: - response["authority_state"]["verdicts"][0]["signer_key_id"] = "f" * 64 - return response - - client, authority, _ = _runtime(response_mutator=mutate) - - with pytest.raises( - DumbMoneyPromotionError, - match="verdict signer is not sealed", - ): - client.bind_intent(authority, _intent(), NOW) - - -def test_resolver_rejects_contract_proof_outside_authority_state() -> None: - def mutate(response: dict[str, Any]) -> dict[str, Any]: - if response["contract_schema"] == PROMOTION_CERTIFICATE_SCHEMA: - response["authority_state"]["promotion_event_digest"] = "f" * 64 - return response - - client, authority, _ = _runtime(response_mutator=mutate) - - with pytest.raises( - DumbMoneyPromotionError, - match="proof event digest", - ): - client.bind_intent(authority, _intent(), NOW) - - -def test_resolver_rejects_authority_state_change_between_lineage_reads() -> None: - calls = 0 - - def mutate(response: dict[str, Any]) -> dict[str, Any]: - nonlocal calls - calls += 1 - if calls == 2: - state = response["authority_state"] - state["ledger_head_sequence"] = 3 - state["ledger_head_digest"] = "f" * 64 - return response - - client, authority, _ = _runtime(response_mutator=mutate) - - with pytest.raises( - DumbMoneyPromotionError, - match="one authority state", - ): - client.bind_intent(authority, _intent(), NOW) diff --git a/tests/test_dumbmoney_direct_mcp.py b/tests/test_dumbmoney_direct_mcp.py deleted file mode 100644 index fe497ab..0000000 --- a/tests/test_dumbmoney_direct_mcp.py +++ /dev/null @@ -1,894 +0,0 @@ -from __future__ import annotations - -import hashlib -import json -import sys -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from types import SimpleNamespace -from typing import Any - -import pytest -import requests - -from dopey_live import service as live_service -from dopey_live import robinhood_gateway as gateway_module -from dopey_live.robinhood_gateway import ( - RobinhoodAgenticGateway, - RobinhoodGatewayError, - RobinhoodPlacementAmbiguousError, -) -from dopey_live.robinhood_mcp_direct import DeterministicRobinhoodMcpClient - - -TOKEN = "sealed-robinhood-profile" -ACCOUNT_ID = "agentic-account-1" -ACCOUNT_HASH = hashlib.sha256(ACCOUNT_ID.encode()).hexdigest() - - -@dataclass -class FakeResponse: - status_code: int - payload: dict[str, Any] | None - headers: dict[str, str] - - @property - def content(self) -> bytes: - if self.payload is None: - return b"" - return json.dumps( - self.payload, - sort_keys=True, - separators=(",", ":"), - ).encode() - - -class FakeSession: - def __init__( - self, - *, - drift_schema: bool = False, - fail_after_place_call: bool = False, - ) -> None: - self.trust_env = True - self.drift_schema = drift_schema - self.fail_after_place_call = fail_after_place_call - self.calls: list[dict[str, Any]] = [] - - def request(self, method: str, url: str, **kwargs: Any) -> FakeResponse: - self.calls.append({"method": method, "url": url, **kwargs}) - payload = kwargs.get("json") - if method == "DELETE": - return FakeResponse(202, None, {}) - if payload["method"] == "initialize": - return FakeResponse( - 200, - { - "jsonrpc": "2.0", - "id": 1, - "result": { - "protocolVersion": "2025-06-18", - "capabilities": {}, - "serverInfo": {"name": "robinhood", "version": "1"}, - }, - }, - { - "Content-Type": "application/json", - "Mcp-Session-Id": "session-1", - }, - ) - if payload["method"] == "notifications/initialized": - return FakeResponse(202, None, {}) - if payload["method"] == "tools/list": - properties = { - "side": {"type": "string"}, - "amount": {"type": "number"}, - "symbol": {"type": "string"}, - "quantity": {"type": "number"}, - "order_type": {"type": "string"}, - "limit_price": {"type": "number"}, - "time_in_force": {"type": "string"}, - } - if self.drift_schema: - properties["destination"] = {"type": "string"} - return FakeResponse( - 200, - { - "jsonrpc": "2.0", - "id": 2, - "result": { - "tools": [ - { - "name": "get_accounts", - "inputSchema": { - "type": "object", - "properties": {}, - "required": [], - }, - }, - { - "name": "place_equity_order", - "inputSchema": { - "type": "object", - "properties": properties, - "required": ["side", "symbol", "order_type"], - }, - }, - ] - }, - }, - {"Content-Type": "application/json"}, - ) - assert payload["method"] == "tools/call" - if payload["params"]["name"] == "get_accounts": - return FakeResponse( - 200, - { - "jsonrpc": "2.0", - "id": payload["id"], - "result": { - "structuredContent": { - "accounts": [ - { - "account_number": ACCOUNT_ID, - "account_label": "Agentic", - "brokerage_account_type": "individual", - } - ] - }, - "isError": False, - }, - }, - {"Content-Type": "application/json"}, - ) - if self.fail_after_place_call: - raise requests.ConnectionError("connection lost after request write") - return FakeResponse( - 200, - { - "jsonrpc": "2.0", - "id": payload["id"], - "result": { - "structuredContent": { - **payload["params"]["arguments"], - "placed": True, - "order_id": "order-1", - "status": "queued", - }, - "isError": False, - }, - }, - {"Content-Type": "application/json"}, - ) - - -class ReadSchemaSession: - def __init__(self, *, type_drift: bool) -> None: - self.trust_env = True - self.type_drift = type_drift - self.calls: list[dict[str, Any]] = [] - - def request(self, method: str, url: str, **kwargs: Any) -> FakeResponse: - self.calls.append({"method": method, "url": url, **kwargs}) - payload = kwargs.get("json") - if method == "DELETE": - return FakeResponse(202, None, {}) - if payload["method"] == "initialize": - return FakeResponse( - 200, - { - "jsonrpc": "2.0", - "id": 1, - "result": { - "protocolVersion": "2025-06-18", - "capabilities": {}, - }, - }, - { - "Content-Type": "application/json", - "Mcp-Session-Id": "read-session-1", - }, - ) - if payload["method"] == "notifications/initialized": - return FakeResponse(202, None, {}) - if payload["method"] == "tools/list": - return FakeResponse( - 200, - { - "jsonrpc": "2.0", - "id": 2, - "result": { - "tools": [ - { - "name": "get_portfolio", - "inputSchema": { - "type": "object", - "properties": { - "account_number": { - "type": ( - "integer" - if self.type_drift - else "string" - ) - } - }, - "required": ["account_number"], - }, - } - ] - }, - }, - {"Content-Type": "application/json"}, - ) - assert payload["method"] == "tools/call" - return FakeResponse( - 200, - { - "jsonrpc": "2.0", - "id": payload["id"], - "result": { - "structuredContent": { - "buying_power": "100", - "total_equity": "100", - }, - "isError": False, - }, - }, - {"Content-Type": "application/json"}, - ) - - -def _place_arguments() -> dict[str, Any]: - return { - "side": "buy", - "symbol": "SPY", - "quantity": 0.125, - "order_type": "limit", - "limit_price": 600.00, - "time_in_force": "day", - } - - -class ParentReadTransport: - def __init__( - self, - *, - account_id: str = ACCOUNT_ID, - buying_power: Any = "1000.00", - total_equity: Any = "1250.00", - equity_positions: list[dict[str, Any]] | None = None, - quote_observed_at: str = "2099-01-01T00:00:00Z", - extra_quote_symbol: str = "", - history_next: str | None = None, - fail_tool: str = "", - ) -> None: - self.account_id = account_id - self.buying_power = buying_power - self.total_equity = total_equity - self.equity_positions = [] if equity_positions is None else equity_positions - self.quote_observed_at = quote_observed_at - self.extra_quote_symbol = extra_quote_symbol - self.history_next = history_next - self.fail_tool = fail_tool - self.calls: list[tuple[str, dict[str, Any], Any]] = [] - - def __call__( - self, - tool: str, - arguments: dict[str, Any], - _timeout: int, - pre_call_guard: Any, - ) -> dict[str, Any]: - self.calls.append((tool, dict(arguments), pre_call_guard)) - if tool == self.fail_tool: - raise RobinhoodGatewayError("parent read failed") - if pre_call_guard is not None: - raise AssertionError("read calls must not carry a write guard") - if tool == "get_accounts": - value: dict[str, Any] = { - "accounts": [ - { - "account_number": self.account_id, - "account_label": "Agentic", - "brokerage_account_type": "individual", - } - ] - } - elif tool == "get_portfolio": - value = { - "buying_power": self.buying_power, - "total_equity": self.total_equity, - } - elif tool == "get_equity_positions": - value = {"positions": self.equity_positions} - elif tool == "get_option_positions": - value = {"positions": []} - elif tool in {"get_equity_orders", "get_option_orders"}: - value = { - "orders": [], - "history_complete": True, - "next": self.history_next, - } - elif tool == "get_equity_quotes": - symbols = [ - *arguments["symbols"], - *([self.extra_quote_symbol] if self.extra_quote_symbol else []), - ] - value = { - "quotes": [ - { - "symbol": symbol, - "bid_price": "99.90", - "ask_price": "100.00", - "last_price": "99.95", - "observed_at": self.quote_observed_at, - } - for symbol in symbols - ] - } - elif tool == "get_equity_tradability": - symbols = [ - *arguments["symbols"], - *([self.extra_quote_symbol] if self.extra_quote_symbol else []), - ] - value = { - "results": [{"symbol": symbol, "tradable": True} for symbol in symbols] - } - else: - raise AssertionError(f"unexpected tool: {tool}") - return {"structuredContent": value, "isError": False} - - -def test_direct_mcp_makes_one_exact_call_after_schema_verification() -> None: - session = FakeSession() - client = DeterministicRobinhoodMcpClient( # type: ignore[arg-type] - TOKEN, - ACCOUNT_HASH, - session, - ) - guard_observations: list[int] = [] - - result = client( - "place_equity_order", - _place_arguments(), - 30, - lambda: guard_observations.append(len(session.calls)), - ) - - assert result["structuredContent"]["order_id"] == "order-1" - tool_calls = [ - call - for call in session.calls - if isinstance(call.get("json"), dict) - and call["json"].get("method") == "tools/call" - and call["json"]["params"]["name"] == "place_equity_order" - ] - assert len(tool_calls) == 1 - assert guard_observations == [4] - assert tool_calls[0]["json"]["params"] == { - "name": "place_equity_order", - "arguments": _place_arguments(), - } - assert session.trust_env is False - assert all(call["allow_redirects"] is False for call in session.calls) - assert all( - call["headers"]["Authorization"] == f"Bearer {TOKEN}" for call in session.calls - ) - - -def test_direct_mcp_blocks_schema_drift_before_the_write_call() -> None: - session = FakeSession(drift_schema=True) - client = DeterministicRobinhoodMcpClient( # type: ignore[arg-type] - TOKEN, - ACCOUNT_HASH, - session, - ) - - with pytest.raises(RobinhoodGatewayError, match="schema drifted"): - client("place_equity_order", _place_arguments(), 30) - - assert not any( - isinstance(call.get("json"), dict) - and call["json"].get("method") == "tools/call" - for call in session.calls - ) - - -def test_direct_mcp_rejects_notional_mode_and_unknown_tools_locally() -> None: - client = DeterministicRobinhoodMcpClient( - TOKEN, - ACCOUNT_HASH, - FakeSession(), # type: ignore[arg-type] - ) - with pytest.raises(RobinhoodGatewayError, match="exact quantity mode"): - client( - "place_equity_order", - { - "side": "buy", - "symbol": "SPY", - "amount": 10.0, - "order_type": "market", - }, - 30, - ) - with pytest.raises(RobinhoodGatewayError, match="outside the sealed allowlist"): - client("place_option_order", {}, 30) - - -def test_direct_read_schema_type_drift_blocks_and_closes_session() -> None: - session = ReadSchemaSession(type_drift=True) - client = DeterministicRobinhoodMcpClient( # type: ignore[arg-type] - TOKEN, - ACCOUNT_HASH, - session, - ) - - with pytest.raises(RobinhoodGatewayError, match="property type drifted"): - client( - "get_portfolio", - {"account_number": ACCOUNT_ID}, - 30, - ) - - assert not any( - isinstance(call.get("json"), dict) - and call["json"].get("method") == "tools/call" - for call in session.calls - ) - assert [call["method"] for call in session.calls].count("DELETE") == 1 - - -def test_direct_mcp_account_mismatch_blocks_before_place() -> None: - session = FakeSession() - client = DeterministicRobinhoodMcpClient( # type: ignore[arg-type] - TOKEN, - "f" * 64, - session, - ) - - with pytest.raises(RobinhoodGatewayError, match="pinned Agentic account"): - client( - "place_equity_order", - _place_arguments(), - 30, - lambda: None, - ) - - assert not any( - isinstance(call.get("json"), dict) - and call["json"].get("method") == "tools/call" - and call["json"]["params"]["name"] == "place_equity_order" - for call in session.calls - ) - - -def test_direct_mcp_connection_loss_after_place_call_is_not_retried() -> None: - session = FakeSession(fail_after_place_call=True) - client = DeterministicRobinhoodMcpClient( # type: ignore[arg-type] - TOKEN, - ACCOUNT_HASH, - session, - ) - - with pytest.raises(RobinhoodGatewayError, match="transport failed"): - client( - "place_equity_order", - _place_arguments(), - 30, - lambda: None, - ) - - place_calls = [ - call - for call in session.calls - if isinstance(call.get("json"), dict) - and call["json"].get("method") == "tools/call" - and call["json"]["params"]["name"] == "place_equity_order" - ] - assert len(place_calls) == 1 - - -def test_gateway_deterministically_reviews_then_places_exact_quantity_once() -> None: - calls: list[tuple[str, dict[str, Any]]] = [] - runner_calls: list[bool] = [] - - def direct_call( - tool: str, - arguments: dict[str, Any], - _timeout: int, - pre_call_guard: Any, - ) -> dict[str, Any]: - if tool == "place_equity_order": - assert pre_call_guard is not None - pre_call_guard() - else: - assert pre_call_guard is None - calls.append((tool, arguments)) - common = { - **arguments, - "warnings": [], - } - if tool == "review_equity_order": - return { - "structuredContent": { - **common, - "review_succeeded": True, - } - } - return { - "structuredContent": { - **common, - "placed": True, - "order_id": "order-1", - "status": "queued", - } - } - - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: runner_calls.append(True), # type: ignore[arg-type] - 60, - write_timeout_seconds=30, - expected_account_hash=ACCOUNT_HASH, - deterministic_mcp_call=direct_call, - ) - intent = SimpleNamespace( - instrument="equity", - side="buy", - symbol="SPY", - quantity=0.125, - notional_usd=75.0, - order_type="limit", - limit_price=600.0, - time_in_force="day", - extended_hours=False, - execution_snapshot=object(), - intent_id="intent-1", - ) - guard_calls: list[bool] = [] - - result = gateway.place_order( # type: ignore[arg-type] - intent, - "a" * 64, - pre_submit_guard=lambda: ( - guard_calls.append(True) - or datetime.now(timezone.utc) + timedelta(seconds=60) - ), - ) - - expected = _place_arguments() - assert calls == [ - ("review_equity_order", expected), - ("place_equity_order", expected), - ] - assert len(guard_calls) == 2 - assert result.placed is True - assert result.order_id == "order-1" - assert runner_calls == [] - - -def test_gateway_marks_post_call_connection_loss_ambiguous_without_retry() -> None: - calls: list[str] = [] - guard_calls: list[bool] = [] - - def direct_call( - tool: str, - arguments: dict[str, Any], - _timeout: int, - pre_call_guard: Any, - ) -> dict[str, Any]: - calls.append(tool) - if tool == "review_equity_order": - return { - "structuredContent": { - **arguments, - "review_succeeded": True, - "warnings": [], - } - } - assert pre_call_guard is not None - pre_call_guard() - raise RobinhoodGatewayError("connection lost after write") - - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError("model runner must never receive broker authority") - ), - 60, - write_timeout_seconds=30, - expected_account_hash=ACCOUNT_HASH, - deterministic_mcp_call=direct_call, - ) - intent = SimpleNamespace( - instrument="equity", - side="buy", - symbol="SPY", - quantity=0.125, - notional_usd=75.0, - order_type="limit", - limit_price=600.0, - time_in_force="day", - extended_hours=False, - execution_snapshot=object(), - intent_id="intent-loss", - ) - - with pytest.raises( - RobinhoodPlacementAmbiguousError, - match="unknown broker outcome", - ): - gateway.place_order( # type: ignore[arg-type] - intent, - "a" * 64, - pre_submit_guard=lambda: ( - guard_calls.append(True) - or datetime.now(timezone.utc) + timedelta(seconds=60) - ), - ) - - assert calls == ["review_equity_order", "place_equity_order"] - assert len(guard_calls) == 2 - - -def test_parent_only_reads_exact_account_and_quote_sequences_without_model() -> None: - transport = ParentReadTransport() - runner_calls: list[bool] = [] - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: runner_calls.append(True), # type: ignore[arg-type] - 60, - expected_account_hash=ACCOUNT_HASH, - deterministic_mcp_call=transport, - ) - - account = gateway.get_account_snapshot() - intent = SimpleNamespace( - instrument="equity", - side="buy", - symbol="SPY", - analysis_symbol="SPY", - ) - quote = gateway.get_equity_execution_snapshot(intent) # type: ignore[arg-type] - - assert account.account_hash == ACCOUNT_HASH - assert account.authenticated is True - assert quote.execution_quote.symbol == "SPY" - assert runner_calls == [] - assert [(tool, arguments) for tool, arguments, _ in transport.calls] == [ - ("get_accounts", {}), - ("get_portfolio", {"account_number": ACCOUNT_ID}), - ("get_equity_positions", {"account_number": ACCOUNT_ID}), - ("get_option_positions", {"account_number": ACCOUNT_ID}), - ( - "get_equity_orders", - {"account_number": ACCOUNT_ID, "limit": 1000}, - ), - ( - "get_option_orders", - {"account_number": ACCOUNT_ID, "limit": 1000}, - ), - ("get_equity_quotes", {"symbols": ["SPY"]}), - ("get_equity_tradability", {"symbols": ["SPY"]}), - ] - - -def test_parent_read_wrong_account_stops_before_scoped_calls() -> None: - transport = ParentReadTransport(account_id="other-agentic-account") - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError("model fallback is prohibited") - ), - 60, - expected_account_hash=ACCOUNT_HASH, - deterministic_mcp_call=transport, - ) - - with pytest.raises(RobinhoodGatewayError, match="pinned Agentic account"): - gateway.get_account_snapshot() - - assert [tool for tool, _, _ in transport.calls] == ["get_accounts"] - - -@pytest.mark.parametrize( - ("buying_power", "total_equity"), - [ - ("-1", "100"), - ("100", "1e9999"), - ], -) -def test_parent_read_rejects_negative_or_overflow_portfolio( - buying_power: str, - total_equity: str, -) -> None: - transport = ParentReadTransport( - buying_power=buying_power, - total_equity=total_equity, - ) - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError("model fallback is prohibited") - ), - 60, - expected_account_hash=ACCOUNT_HASH, - deterministic_mcp_call=transport, - ) - - with pytest.raises(RobinhoodGatewayError, match="finite and nonnegative"): - gateway.get_account_snapshot() - - -def test_parent_read_does_not_zero_fill_missing_position_economics() -> None: - transport = ParentReadTransport( - equity_positions=[ - { - "symbol": "SPY", - "quantity": "1", - "market_price": "100", - "market_value": "100", - "unrealized_pnl": "0", - } - ] - ) - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError("model fallback is prohibited") - ), - 60, - expected_account_hash=ACCOUNT_HASH, - deterministic_mcp_call=transport, - ) - - with pytest.raises(RobinhoodGatewayError, match="position.average_cost"): - gateway.get_account_snapshot() - - -def test_parent_read_rejects_conflicting_position_aliases() -> None: - transport = ParentReadTransport( - equity_positions=[ - { - "symbol": "SPY", - "quantity": "1", - "average_cost": "99", - "average_price": "100", - "market_price": "100", - "market_value": "100", - "unrealized_pnl": "1", - } - ] - ) - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError("model fallback is prohibited") - ), - 60, - expected_account_hash=ACCOUNT_HASH, - deterministic_mcp_call=transport, - ) - - with pytest.raises(RobinhoodGatewayError, match="conflicting numeric aliases"): - gateway.get_account_snapshot() - - -def test_parent_read_preserves_incomplete_history_as_blocking_evidence() -> None: - transport = ParentReadTransport(history_next="next-page") - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError("model fallback is prohibited") - ), - 60, - expected_account_hash=ACCOUNT_HASH, - deterministic_mcp_call=transport, - ) - - account = gateway.get_account_snapshot() - - assert account.equity_order_history_complete is False - assert account.option_order_history_complete is False - assert "equity_order_history_incomplete" in account.error - assert "option_order_history_incomplete" in account.error - - -def test_parent_read_rejects_naive_quote_timestamp_and_never_falls_back() -> None: - transport = ParentReadTransport( - quote_observed_at="2099-01-01T00:00:00", - ) - runner_calls: list[bool] = [] - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: runner_calls.append(True), # type: ignore[arg-type] - 60, - expected_account_hash=ACCOUNT_HASH, - deterministic_mcp_call=transport, - ) - intent = SimpleNamespace( - instrument="equity", - side="buy", - symbol="SPY", - analysis_symbol="SPY", - ) - - with pytest.raises(RobinhoodGatewayError, match="timezone-aware"): - gateway.get_equity_execution_snapshot(intent) # type: ignore[arg-type] - - assert runner_calls == [] - - -def test_parent_read_rejects_unrequested_quote_symbol() -> None: - transport = ParentReadTransport(extra_quote_symbol="QQQ") - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError("model fallback is prohibited") - ), - 60, - expected_account_hash=ACCOUNT_HASH, - deterministic_mcp_call=transport, - ) - intent = SimpleNamespace( - instrument="equity", - side="buy", - symbol="SPY", - analysis_symbol="SPY", - ) - - with pytest.raises(RobinhoodGatewayError, match="unrequested symbol"): - gateway.get_equity_execution_snapshot(intent) # type: ignore[arg-type] - - -def test_parent_read_transport_failure_never_falls_back_to_model() -> None: - transport = ParentReadTransport(fail_tool="get_accounts") - runner_calls: list[bool] = [] - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: runner_calls.append(True), # type: ignore[arg-type] - 60, - expected_account_hash=ACCOUNT_HASH, - deterministic_mcp_call=transport, - ) - - with pytest.raises(RobinhoodGatewayError, match="parent read failed"): - gateway.get_account_snapshot() - - assert runner_calls == [] - - -def test_operator_default_gateway_cannot_place_or_cancel_through_model( - monkeypatch: pytest.MonkeyPatch, -) -> None: - runner_calls: list[bool] = [] - monkeypatch.setattr( - live_service.subprocess, - "run", - lambda *_args, **_kwargs: runner_calls.append(True), - ) - monkeypatch.setattr( - gateway_module.shutil, - "which", - lambda executable: sys.executable if executable == "codex" else None, - ) - gateway = live_service.default_gateway() - intent = SimpleNamespace( - instrument="equity", - side="sell", - symbol="SPY", - quantity=0.125, - notional_usd=0.0, - order_type="limit", - limit_price=600.0, - time_in_force="day", - extended_hours=False, - execution_snapshot=None, - intent_id="legacy-denied", - ) - - with pytest.raises( - RobinhoodGatewayError, - match="deterministic parent-side MCP", - ): - gateway.place_order( # type: ignore[arg-type] - intent, - "a" * 64, - pre_submit_guard=lambda: datetime.now(timezone.utc) + timedelta(seconds=60), - ) - with pytest.raises( - RobinhoodGatewayError, - match="deterministic parent-side MCP", - ): - gateway.cancel_order("equity", "order-1") - - assert runner_calls == [] diff --git a/tests/test_dumbmoney_journal_anchor.py b/tests/test_dumbmoney_journal_anchor.py deleted file mode 100644 index 38c5841..0000000 --- a/tests/test_dumbmoney_journal_anchor.py +++ /dev/null @@ -1,236 +0,0 @@ -from __future__ import annotations - -import base64 -import hashlib -import json -from datetime import UTC, datetime, timedelta -from typing import Any - -import pytest -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -from dopey_live.dumbmoney_authority import canonical_json_bytes -from dopey_live.dumbmoney_command_feed import ( - CommandFeedHttpRequest, - CommandFeedHttpResponse, -) -from dopey_live.dumbmoney_journal_anchor import ( - ANCHOR_BODY_SCHEMA, - ANCHOR_CHECKPOINT_SCHEMA, - ANCHOR_RESPONSE_SCHEMA, - CoreJournalAnchorClient, - JournalAnchorError, -) - -NOW = datetime(2026, 7, 26, 22, 0, 0, tzinfo=UTC) -ACCOUNT_HASH = hashlib.sha256(b"dopey-account").hexdigest() -PRIVATE_KEY = Ed25519PrivateKey.from_private_bytes(bytes(range(32))) -PUBLIC_KEY = PRIVATE_KEY.public_key().public_bytes_raw() -SIGNER_KEY_ID = hashlib.sha256(PUBLIC_KEY).hexdigest() -ZERO_DIGEST = "0" * 64 - - -def _z(value: datetime) -> str: - return value.astimezone(UTC).isoformat().replace("+00:00", "Z") - - -def _hash(value: str | dict[str, Any]) -> str: - material = ( - value.encode("utf-8") if isinstance(value, str) else canonical_json_bytes(value) - ) - return hashlib.sha256(material).hexdigest() - - -def _signature(value: dict[str, Any]) -> str: - return ( - base64.urlsafe_b64encode(PRIVATE_KEY.sign(canonical_json_bytes(value))) - .decode("ascii") - .rstrip("=") - ) - - -def _signed_anchor( - body: dict[str, Any], - observed_at: datetime, -) -> dict[str, Any]: - unsigned = { - "schema": "dumbmoney.signed-envelope.v1", - "source_id": "dumbmoney-core", - "source_sequence": 1, - "correlation_id": "journal-anchor:dopey_robinhood:test-journal", - "causation_id": None, - "nonce": _hash("anchor-event-nonce"), - "not_before": _z(observed_at), - "expires_at": _z(observed_at + timedelta(seconds=120)), - "body_schema": ANCHOR_BODY_SCHEMA, - "body_digest": _hash(body), - "body": body, - "signature_algorithm": "Ed25519", - "signer_key_id": SIGNER_KEY_ID, - } - envelope = {**unsigned, "event_id": _hash(unsigned)} - envelope["signature"] = _signature(envelope) - return envelope - - -def _response( - request: dict[str, Any], - *, - observed_at: datetime = NOW, -) -> dict[str, Any]: - identity = { - "schema": ANCHOR_BODY_SCHEMA, - "venue": "dopey_robinhood", - "account_hash": request["account_hash"], - "journal_name": request["journal_name"], - "journal_schema": request["journal_schema"], - "journal_stream_id": request["journal_stream_id"], - "journal_sequence": request["journal_sequence"], - "journal_head_sha256": request["journal_head_sha256"], - "previous_anchor_body_digest": ZERO_DIGEST, - } - body = { - **identity, - "anchor_id": _hash(identity), - "anchored_at": _z(observed_at), - } - envelope = _signed_anchor(body, observed_at) - event_material = { - "global_sequence": 1, - "event_id": envelope["event_id"], - "source_id": envelope["source_id"], - "source_sequence": envelope["source_sequence"], - "signer_key_id": envelope["signer_key_id"], - "nonce": envelope["nonce"], - "event_schema": envelope["body_schema"], - "observed_at": _z(observed_at), - "received_at": _z(observed_at), - "correlation_id": envelope["correlation_id"], - "causation_id": envelope["causation_id"], - "payload_digest": envelope["body_digest"], - "previous_source_digest": ZERO_DIGEST, - "previous_global_digest": ZERO_DIGEST, - } - event_digest = _hash(event_material) - checkpoint = { - "schema": ANCHOR_CHECKPOINT_SCHEMA, - "cell_id": "dopey_robinhood", - "request_nonce": request["request_nonce"], - "account_hash": request["account_hash"], - "journal_name": request["journal_name"], - "journal_schema": request["journal_schema"], - "journal_stream_id": request["journal_stream_id"], - "journal_sequence": request["journal_sequence"], - "journal_head_sha256": request["journal_head_sha256"], - "anchor_body_digest": envelope["body_digest"], - "anchor_event_digest": event_digest, - "reused": False, - "observed_at": _z(observed_at), - } - return { - "schema": ANCHOR_RESPONSE_SCHEMA, - "cell_id": "dopey_robinhood", - "request_nonce": request["request_nonce"], - "observed_at": _z(observed_at), - "reused": False, - "anchor_envelope": envelope, - "ledger_proof": { - "schema": "dumbmoney.ledger-event-proof.v1", - **event_material, - "event_digest": event_digest, - }, - "checkpoint": checkpoint, - "checkpoint_signature": { - "algorithm": "Ed25519", - "signer_key_id": SIGNER_KEY_ID, - "signature": _signature(checkpoint), - }, - } - - -class _Transport: - def __init__(self, mutate: Any = None, *, status: int = 200) -> None: - self.mutate = mutate - self.status = status - self.calls: list[CommandFeedHttpRequest] = [] - - def __call__( - self, - request: CommandFeedHttpRequest, - ) -> CommandFeedHttpResponse: - self.calls.append(request) - assert request.body is not None - payload = _response(json.loads(request.body)) - if self.mutate is not None: - self.mutate(payload) - return CommandFeedHttpResponse( - status=self.status, - headers={"Content-Type": "application/json"}, - body=canonical_json_bytes(payload), - ) - - -def _client(transport: Any, now: datetime = NOW) -> CoreJournalAnchorClient: - return CoreJournalAnchorClient( - base_url="http://127.0.0.1:49152", - transport=transport, - cell_token_provider=lambda: "t" * 64, - trusted_core_public_key=PUBLIC_KEY, - expected_account_hash=ACCOUNT_HASH, - request_nonce_fn=lambda: _hash("request-nonce"), - now_fn=lambda: now, - ) - - -def test_exact_core_anchor_checkpoint_and_proof_are_accepted() -> None: - transport = _Transport() - client = _client(transport) - head = _hash("journal-head") - - result = client.anchor( - journal_name="live-branch-ledger", - journal_schema="dopey-live-branch-ledger-v1", - journal_sequence=7, - journal_head_sha256=head, - ) - - assert result.journal_sequence == 7 - assert result.journal_head_sha256 == head - assert len(transport.calls) == 1 - request = transport.calls[0] - assert request.method == "POST" - assert request.url.endswith("/v1/cells/dopey_robinhood/journal-heads:anchor") - assert request.headers["Authorization"] == f"Bearer {'t' * 64}" - - -def test_tampered_checkpoint_and_rejected_anchor_fail_closed() -> None: - def tamper(payload: dict[str, Any]) -> None: - payload["checkpoint"]["journal_sequence"] = 6 - - with pytest.raises(JournalAnchorError, match="signature is invalid"): - _client(_Transport(tamper)).anchor( - journal_name="live-branch-ledger", - journal_schema="dopey-live-branch-ledger-v1", - journal_sequence=7, - journal_head_sha256=_hash("journal-head"), - ) - - with pytest.raises(JournalAnchorError, match="Core rejected"): - _client(_Transport(status=409)).anchor( - journal_name="command-feed", - journal_schema="dopey.dumbmoney-command-feed-state.v1", - journal_sequence=0, - journal_head_sha256=ZERO_DIGEST, - ) - - -def test_zero_sequence_and_head_must_move_together() -> None: - client = _client(_Transport()) - - with pytest.raises(JournalAnchorError, match="zero must exactly bind"): - client.anchor( - journal_name="command-feed", - journal_schema="dopey.dumbmoney-command-feed-state.v1", - journal_sequence=0, - journal_head_sha256=_hash("not-zero"), - ) diff --git a/tests/test_dumbmoney_live_authority.py b/tests/test_dumbmoney_live_authority.py deleted file mode 100644 index f39a4de..0000000 --- a/tests/test_dumbmoney_live_authority.py +++ /dev/null @@ -1,1126 +0,0 @@ -from __future__ import annotations - -import base64 -import hashlib -import json -import sys -from dataclasses import replace -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import Any - -import pytest -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -from dopey_live.dumbmoney_authority import ( - DumbMoneyAuthorityContext, - DumbMoneyAuthorityError, - account_open_risk_cents, - authorize_intent, - authorized_settled_cash_usd, - canonical_json_bytes, - verify_signed_capital_envelope, - _urlsafe_b64decode, -) -from dopey_live.ledger import ( - LiveBranchLedgerError, - activate_dumbmoney_fence, - append_record, - assert_current_dumbmoney_fence, - ledger_path, - read_ledger, -) -from dopey_live.models import ( - AccountSnapshot, - GatewayCancelResult, - LiveExecutionIntent, -) -from dopey_live.robinhood_gateway import ( - McpToolCallEvidence, - RobinhoodAgenticGateway, - RobinhoodCancellationAmbiguousError, - RobinhoodGatewayError, - _canonical_raw_broker_item, - _gateway_cancel_result, - _validated_equity_position, -) -from dopey_live.service import ( - _poll_ambiguous_placement_history, - _run_cancel_only_degraded, - _witnessed_daily_loss_cents, -) -from dopey_live.signals import LiveSignalError, validate_option_entry_for_execution -from operator_tools import operator_live_branch - - -FIXTURE_PATH = ( - Path(__file__).parent / "fixtures" / "dumbmoney" / "signed-capital-envelope.v1.json" -) -NEGATIVE_CAP_FIXTURE_PATH = ( - Path(__file__).parent - / "fixtures" - / "dumbmoney" - / "capital-envelope-cap-order-negative.v1.json" -) -FIXTURE_FILE_SHA256 = "b0fe2fa0261b2dc36e4043e8c33b91b1016676e04a25e4b4d5bd66eea0c9079a" -NOW = datetime(2099, 1, 1, 0, 0, 30, tzinfo=timezone.utc) - - -def _fixture() -> tuple[dict[str, Any], bytes, dict[str, Any]]: - fixture = json.loads(FIXTURE_PATH.read_text(encoding="utf-8")) - public_key = base64.urlsafe_b64decode( - fixture["public_key_base64url"] - + "=" * (-len(fixture["public_key_base64url"]) % 4) - ) - return fixture["envelope"], public_key, fixture - - -def _account( - *, - orders: tuple[dict[str, Any], ...] = (), - positions: tuple[dict[str, Any], ...] = (), -) -> AccountSnapshot: - return AccountSnapshot( - authenticated=True, - account_type="individual", - buying_power_usd=100.0, - total_equity_usd=100.0, - equity_positions=positions, - option_positions=(), - recent_equity_orders=orders, - recent_option_orders=(), - equity_order_history_complete=True, - option_order_history_complete=True, - error="", - account_hash="c" * 64, - ) - - -def _equity_intent( - *, - notional_usd: float = 1.0, - quantity: float = 0.01, - promotion_hash: str | None = None, - passport_hash: str | None = None, -) -> LiveExecutionIntent: - return LiveExecutionIntent( - intent_id="intent-1", - instrument="equity", - symbol="ACME", - side="buy", - order_type="limit", - limit_price=100.0, - time_in_force="day", - extended_hours=False, - notional_usd=notional_usd, - quantity=quantity, - source_as_of_date="2098-12-31", - source_hash="a" * 64, - thesis_direction="bullish", - analysis_symbol="ACME", - analysis_relationship="self", - analysis_reference_price=100.0, - analysis_invalidation_price=90.0, - analysis_target_price=120.0, - daily_leverage_multiple=1.0, - daily_reset=False, - maximum_holding_sessions=5, - reference_price=100.0, - invalidation_price=90.0, - target_price=120.0, - expected_hold_trading_days=5, - execution_snapshot=None, - option_contract={}, - reason_codes=("fixture",), - promotion_hash=( - promotion_hash - or str(_fixture()[0]["body"].get("promotion_hashes", ["b" * 64])[0]) - ), - passport_hash=( - passport_hash - or str(_fixture()[0]["body"].get("passport_hashes", ["b" * 64])[0]) - ), - ) - - -def test_blunder_signed_fixture_cross_validates_byte_for_byte() -> None: - envelope, public_key, fixture = _fixture() - signing_payload = { - key: value for key, value in envelope.items() if key != "signature" - } - - assert hashlib.sha256(FIXTURE_PATH.read_bytes()).hexdigest() == ( - FIXTURE_FILE_SHA256 - ) - assert hashlib.sha256(canonical_json_bytes(signing_payload)).hexdigest() == ( - "97f8ecdf0d6b48b310c06cdeb37654fb171f4fa62bf91b427965191643d7ee1e" - ) - assert fixture["signing_payload_sha256"] == ( - "97f8ecdf0d6b48b310c06cdeb37654fb171f4fa62bf91b427965191643d7ee1e" - ) - assert envelope["body_digest"] == ( - "fcf86b4c257c0fea9ba65051366b76738563598d4217c4950ef3cf63ed1927d0" - ) - assert envelope["event_id"] == ( - "95fd19cc7bcbc89fe3c7985ee04d502704344b2f09c1eb6dce575b41c0ae579c" - ) - assert envelope["body"]["promotion_hashes"] == ["f" * 64] - assert envelope["body"]["authorized_instruments"] == ["equity:ACME"] - authority = verify_signed_capital_envelope( - envelope, - public_key, - NOW, - "c" * 64, - ) - - assert authority.event_id == envelope["event_id"] - assert authority.envelope.venue == "dopey_robinhood" - assert authority.envelope.fencing_generation == 7 - - -def test_signed_envelope_rejects_tamper_expiry_and_wrong_account() -> None: - envelope, public_key, _ = _fixture() - tampered = json.loads(json.dumps(envelope)) - tampered["body"]["max_order_risk_cents"] = 200 - - with pytest.raises(DumbMoneyAuthorityError, match="body digest mismatch"): - verify_signed_capital_envelope(tampered, public_key, NOW, "c" * 64) - with pytest.raises(DumbMoneyAuthorityError, match="not currently valid"): - verify_signed_capital_envelope( - envelope, - public_key, - datetime(2099, 1, 1, 0, 1, tzinfo=timezone.utc), - "c" * 64, - ) - with pytest.raises(DumbMoneyAuthorityError, match="different account"): - verify_signed_capital_envelope(envelope, public_key, NOW, "d" * 64) - - -def test_authority_file_requires_strict_canonical_json(tmp_path: Path) -> None: - envelope, public_key, _ = _fixture() - envelope_path = tmp_path / "capital-envelope.json" - public_key_path = tmp_path / "core.pub" - envelope_path.write_bytes(canonical_json_bytes(envelope)) - public_key_path.write_bytes(public_key) - - context = DumbMoneyAuthorityContext.from_files( - envelope_path, - public_key_path, - "c" * 64, - ) - - assert context.signed_envelope == envelope - assert context.trusted_public_key == public_key - - -@pytest.mark.parametrize( - "raw, match", - [ - ( - b'{"schema":"first","schema":"second"}', - "duplicate JSON key", - ), - ( - b'{"body":{"schema":"first","schema":"second"}}', - "duplicate JSON key", - ), - ( - b'{"value":NaN}', - "non-finite JSON number", - ), - ( - b'{"value":Infinity}', - "non-finite JSON number", - ), - ( - b'{"value":1e9999}', - "non-finite JSON number", - ), - ( - b'{"value":1.5}', - "float outside the signed integer domain", - ), - ], -) -def test_authority_file_rejects_ambiguous_json( - tmp_path: Path, - raw: bytes, - match: str, -) -> None: - envelope_path = tmp_path / "capital-envelope.json" - public_key_path = tmp_path / "core.pub" - envelope_path.write_bytes(raw) - public_key_path.write_bytes(b"x" * 32) - - with pytest.raises(DumbMoneyAuthorityError, match=match): - DumbMoneyAuthorityContext.from_files( - envelope_path, - public_key_path, - "c" * 64, - ) - - -def test_authority_file_rejects_noncanonical_bytes(tmp_path: Path) -> None: - envelope, public_key, _ = _fixture() - envelope_path = tmp_path / "capital-envelope.json" - public_key_path = tmp_path / "core.pub" - envelope_path.write_text( - json.dumps(envelope, indent=2, ensure_ascii=False) + "\n", - encoding="utf-8", - ) - public_key_path.write_bytes(public_key) - - with pytest.raises(DumbMoneyAuthorityError, match="exact canonical JSON bytes"): - DumbMoneyAuthorityContext.from_files( - envelope_path, - public_key_path, - "c" * 64, - ) - - -def test_monotonic_fence_survives_local_lock_reacquisition(tmp_path: Path) -> None: - activate_dumbmoney_fence( - tmp_path, - "e" * 64, - "1" * 64, - "2" * 64, - 7, - 1, - ) - activate_dumbmoney_fence( - tmp_path, - "e" * 64, - "1" * 64, - "2" * 64, - 7, - 1, - ) - assert_current_dumbmoney_fence( - tmp_path, - "e" * 64, - "1" * 64, - "2" * 64, - 7, - 1, - ) - - with pytest.raises(LiveBranchLedgerError, match="newer generation"): - activate_dumbmoney_fence( - tmp_path, - "f" * 64, - "2" * 64, - "3" * 64, - 6, - 1, - ) - with pytest.raises(LiveBranchLedgerError, match="reused"): - activate_dumbmoney_fence( - tmp_path, - "e" * 64, - "9" * 64, - "2" * 64, - 7, - 1, - ) - with pytest.raises(LiveBranchLedgerError, match="reused"): - activate_dumbmoney_fence( - tmp_path, - "e" * 64, - "1" * 64, - "8" * 64, - 7, - 1, - ) - - activate_dumbmoney_fence( - tmp_path, - "f" * 64, - "3" * 64, - "4" * 64, - 8, - 2, - ) - with pytest.raises(LiveBranchLedgerError, match="no longer"): - assert_current_dumbmoney_fence( - tmp_path, - "e" * 64, - "1" * 64, - "2" * 64, - 7, - 1, - ) - - -def test_capital_intersects_settled_cash_open_risk_and_order_limit() -> None: - envelope, public_key, _ = _fixture() - authority = verify_signed_capital_envelope(envelope, public_key, NOW, "c" * 64) - account = _account() - - assert authorized_settled_cash_usd(authority, account, 100.0, 0) == 3.0 - assert authorize_intent(authority, account, _equity_intent(), 0) == 100 - - with pytest.raises(DumbMoneyAuthorityError, match="max_order_risk"): - authorize_intent( - authority, - account, - _equity_intent(notional_usd=2.0, quantity=0.02), - 0, - ) - exposed = _account( - positions=( - { - "symbol": "ACME", - "quantity": 1.0, - "average_cost": 2.5, - "market_price": 2.5, - "market_value": 2.5, - "unrealized_pnl": 0.0, - }, - ) - ) - assert account_open_risk_cents(exposed) == 250 - assert authorized_settled_cash_usd(authority, exposed, 100.0, 0) == 0.5 - - -def test_intent_requires_exact_instrument_promotion_and_daily_loss_headroom() -> None: - envelope, public_key, _ = _fixture() - authority = verify_signed_capital_envelope(envelope, public_key, NOW, "c" * 64) - intent = _equity_intent() - - assert authorize_intent(authority, _account(), intent, 200) == 100 - with pytest.raises(DumbMoneyAuthorityError, match="daily-loss capacity"): - authorize_intent(authority, _account(), intent, 201) - with pytest.raises(DumbMoneyAuthorityError, match="daily-loss capacity"): - authorize_intent(authority, _account(), intent, 299) - with pytest.raises(DumbMoneyAuthorityError, match="promotion hash"): - authorize_intent( - authority, - _account(), - replace(intent, promotion_hash="e" * 64), - 0, - ) - with pytest.raises(DumbMoneyAuthorityError, match="passport hash"): - authorize_intent( - authority, - _account(), - replace(intent, passport_hash="e" * 64), - 0, - ) - with pytest.raises(DumbMoneyAuthorityError, match="exact broker instrument"): - authorize_intent( - authority, - _account(), - replace( - intent, - symbol="OTHER", - analysis_symbol="OTHER", - ), - 0, - ) - - -def test_sell_requires_exact_fresh_long_inventory_and_cannot_oversell() -> None: - envelope, public_key, _ = _fixture() - authority = verify_signed_capital_envelope(envelope, public_key, NOW, "c" * 64) - exit_intent = replace( - _equity_intent(quantity=1.0), - side="sell", - notional_usd=0.0, - ) - holding = { - "symbol": "ACME", - "quantity": 1.0, - "average_cost": 100.0, - "market_price": 99.0, - "market_value": 99.0, - "unrealized_pnl": -1.0, - } - - assert ( - authorize_intent( - authority, - _account(positions=(holding,)), - exit_intent, - None, - ) - == 0 - ) - with pytest.raises(DumbMoneyAuthorityError, match="matching long"): - authorize_intent(authority, _account(), exit_intent, None) - with pytest.raises(DumbMoneyAuthorityError, match="oversell"): - authorize_intent( - authority, - _account(positions=(holding,)), - replace(exit_intent, quantity=1.01), - None, - ) - active_sell = { - "order_id": "sell-open", - "symbol": "ACME", - "side": "sell", - "status": "open", - "market_date": "2099-01-01", - "submitted_at": "2099-01-01T00:00:00Z", - "filled_at": "", - "filled_quantity": 0.0, - "average_fill_price": 0.0, - } - with pytest.raises(DumbMoneyAuthorityError, match="active sell"): - authorize_intent( - authority, - _account(orders=(active_sell,), positions=(holding,)), - exit_intent, - None, - ) - - -def test_daily_loss_and_existing_entry_order_fail_closed() -> None: - envelope, public_key, _ = _fixture() - authority = verify_signed_capital_envelope(envelope, public_key, NOW, "c" * 64) - - with pytest.raises(DumbMoneyAuthorityError, match="daily loss is required"): - authorized_settled_cash_usd(authority, _account(), 100.0, None) - with pytest.raises(DumbMoneyAuthorityError, match="max_daily_loss"): - authorize_intent(authority, _account(), _equity_intent(), 300) - - active_entry = { - "order_id": "open-buy", - "symbol": "OTHER", - "side": "buy", - "status": "open", - "market_date": "2099-01-01", - "submitted_at": "2099-01-01T00:00:00+00:00", - "filled_at": "", - "filled_quantity": 0.0, - "average_fill_price": 0.0, - } - with pytest.raises(DumbMoneyAuthorityError, match="active entry order"): - authorize_intent( - authority, - _account(orders=(active_entry,)), - _equity_intent(), - 0, - ) - - -def test_option_position_risk_uses_multiplier_and_rejects_missing_evidence() -> None: - option_position = { - "symbol": "ACME", - "contract_id": "option-1", - "quantity": 2.0, - "average_cost": 1.25, - "market_price": 1.0, - "market_value": 200.0, - "unrealized_pnl": -50.0, - } - account = AccountSnapshot( - **{ - **_account().to_dict(), - "option_positions": (option_position,), - } - ) - assert account_open_risk_cents(account) == 25_000 - - incomplete = AccountSnapshot( - **{ - **_account().to_dict(), - "option_positions": ( - { - "symbol": "ACME", - "contract_id": "option-1", - "quantity": 1.0, - }, - ), - } - ) - with pytest.raises(DumbMoneyAuthorityError, match="complete finite risk"): - account_open_risk_cents(incomplete) - - -def _signed_capital_body( - body: dict[str, Any], - wrapper_not_before: str, - wrapper_expires_at: str, -) -> tuple[dict[str, Any], bytes]: - private_key = Ed25519PrivateKey.generate() - public_key = private_key.public_key().public_bytes_raw() - unsigned: dict[str, Any] = { - "schema": "dumbmoney.signed-envelope.v1", - "source_id": "dumbmoney-core", - "source_sequence": 1, - "correlation_id": "window-test", - "causation_id": None, - "nonce": "window-test-1", - "not_before": wrapper_not_before, - "expires_at": wrapper_expires_at, - "body_schema": "dumbmoney.capital-envelope.v1", - "body_digest": hashlib.sha256(canonical_json_bytes(body)).hexdigest(), - "body": body, - "signature_algorithm": "Ed25519", - "signer_key_id": hashlib.sha256(public_key).hexdigest(), - } - event_material = dict(unsigned) - unsigned["event_id"] = hashlib.sha256( - canonical_json_bytes(event_material) - ).hexdigest() - signature = private_key.sign(canonical_json_bytes(unsigned)) - return { - **unsigned, - "signature": base64.urlsafe_b64encode(signature).decode().rstrip("="), - }, public_key - - -def _signed_envelope_for_window( - wrapper_not_before: str, - wrapper_expires_at: str, - body_not_before: str, - body_expires_at: str, -) -> tuple[dict[str, Any], bytes]: - body = { - **_fixture()[0]["body"], - "not_before": body_not_before, - "expires_at": body_expires_at, - "account_hash": "c" * 64, - } - return _signed_capital_body( - body, - wrapper_not_before, - wrapper_expires_at, - ) - - -def test_capital_envelope_cap_order_negative_fixture_fails_closed() -> None: - fixture = json.loads(NEGATIVE_CAP_FIXTURE_PATH.read_text(encoding="utf-8")) - assert fixture["fixture_schema"] == ( - "dopey.dumbmoney-capital-envelope-negative-cases.v1" - ) - cases = fixture["cases"] - assert [case["name"] for case in cases] == [ - "max_order_exceeds_max_correlated", - "max_correlated_exceeds_max_open", - ] - for case in cases: - body = { - **_fixture()[0]["body"], - **case["overrides"], - } - envelope, public_key = _signed_capital_body( - body, - "2099-01-01T00:00:00Z", - "2099-01-01T00:01:00Z", - ) - with pytest.raises( - DumbMoneyAuthorityError, - match=str(case["expected_error"]), - ): - verify_signed_capital_envelope( - envelope, - public_key, - NOW, - "c" * 64, - ) - - -def test_capital_envelope_rejects_mix_and_match_lineage_sets() -> None: - body = { - **_fixture()[0]["body"], - "strategy_hashes": ["a" * 64, "b" * 64], - "passport_hashes": ["c" * 64, "d" * 64], - "promotion_hashes": ["e" * 64, "f" * 64], - } - envelope, public_key = _signed_capital_body( - body, - "2099-01-01T00:00:00Z", - "2099-01-01T00:01:00Z", - ) - - with pytest.raises( - DumbMoneyAuthorityError, - match="exactly one bound strategy/passport/promotion tuple", - ): - verify_signed_capital_envelope( - envelope, - public_key, - NOW, - "c" * 64, - ) - - -def test_signature_encoding_and_authority_window_are_canonical_and_short() -> None: - envelope, _, _ = _fixture() - signature = envelope["signature"] - with pytest.raises(ValueError, match="canonical"): - _urlsafe_b64decode(signature[:-1] + "x") - - wider, public_key = _signed_envelope_for_window( - "2099-01-01T00:00:00Z", - "2099-01-01T00:02:00Z", - "2099-01-01T00:00:00Z", - "2099-01-01T00:02:00Z", - ) - with pytest.raises(DumbMoneyAuthorityError, match="60-second"): - verify_signed_capital_envelope(wider, public_key, NOW, "c" * 64) - - mismatched, public_key = _signed_envelope_for_window( - "2099-01-01T00:00:00Z", - "2099-01-01T00:01:00Z", - "2099-01-01T00:00:01Z", - "2099-01-01T00:01:00Z", - ) - with pytest.raises(DumbMoneyAuthorityError, match="must match exactly"): - verify_signed_capital_envelope(mismatched, public_key, NOW, "c" * 64) - - -def test_daily_loss_is_derived_from_attributed_fee_bearing_broker_fills( - tmp_path: Path, -) -> None: - buy = _equity_intent(notional_usd=100.0, quantity=1.0) - sell = replace( - buy, - intent_id="intent-sell", - side="sell", - notional_usd=0.0, - quantity=1.0, - ) - for order_id, intent in (("buy-1", buy), ("sell-1", sell)): - append_record( - ledger_path(tmp_path), - "live_order_result", - { - "intent_id": intent.intent_id, - "instrument": intent.instrument, - "symbol": intent.symbol, - "side": intent.side, - "market_date": "2099-01-01", - "intent": intent.to_dict(), - "placed": True, - "order_id": order_id, - "status": "filled", - "filled_quantity": 1.0, - "average_fill_price": 100.0 if intent.side == "buy" else 98.0, - }, - ) - broker_orders = ( - { - "order_id": "buy-1", - "symbol": "ACME", - "side": "buy", - "status": "filled", - "market_date": "2099-01-01", - "submitted_at": "2099-01-01T00:00:10+00:00", - "filled_at": "2099-01-01T00:00:11+00:00", - "filled_quantity": 1.0, - "average_fill_price": 100.0, - "fees_usd": 0.10, - }, - { - "order_id": "sell-1", - "symbol": "ACME", - "side": "sell", - "status": "filled", - "market_date": "2099-01-01", - "submitted_at": "2099-01-01T00:00:20+00:00", - "filled_at": "2099-01-01T00:00:21+00:00", - "filled_quantity": 1.0, - "average_fill_price": 98.0, - "fees_usd": 0.10, - }, - ) - assert ( - _witnessed_daily_loss_cents( - tmp_path, - _account(orders=broker_orders), - "2099-01-01", - ) - == 220 - ) - losing_and_winning_positions = ( - { - "symbol": "LOSS", - "quantity": 1.0, - "average_cost": 10.0, - "market_price": 5.0, - "market_value": 5.0, - "unrealized_pnl": -5.0, - }, - { - "symbol": "GAIN", - "quantity": 1.0, - "average_cost": 10.0, - "market_price": 20.0, - "market_value": 20.0, - "unrealized_pnl": 10.0, - }, - ) - assert ( - _witnessed_daily_loss_cents( - tmp_path, - _account(orders=broker_orders, positions=losing_and_winning_positions), - "2099-01-01", - ) - == 720 - ) - missing_unrealized = ( - { - "symbol": "LOSS", - "quantity": 1.0, - "average_cost": 10.0, - "market_price": 5.0, - "market_value": 5.0, - }, - ) - with pytest.raises(DumbMoneyAuthorityError, match="unrealized PnL"): - _witnessed_daily_loss_cents( - tmp_path, - _account(orders=broker_orders, positions=missing_unrealized), - "2099-01-01", - ) - - missing_fee = tuple( - {key: value for key, value in order.items() if key != "fees_usd"} - for order in broker_orders - ) - with pytest.raises(DumbMoneyAuthorityError, match="fee observation"): - _witnessed_daily_loss_cents( - tmp_path, - _account(orders=missing_fee), - "2099-01-01", - ) - - -def test_raw_broker_position_cannot_fabricate_missing_unrealized_pnl() -> None: - canonical = _canonical_raw_broker_item( - { - "symbol": "ACME", - "quantity": 1.0, - "average_cost": 100.0, - "market_price": 99.0, - "market_value": 99.0, - }, - "equity_position", - ) - - assert "unrealized_pnl" not in canonical - with pytest.raises(RobinhoodGatewayError, match="unrealized_pnl"): - _validated_equity_position(canonical, 0) - - -def test_gateway_final_guard_denies_before_external_write( - monkeypatch: pytest.MonkeyPatch, -) -> None: - gateway = object.__new__(RobinhoodAgenticGateway) - gateway._write_timeout_seconds = 30 - run_calls: list[str] = [] - - def unexpected_run(*_args: Any, **_kwargs: Any) -> None: - run_calls.append("called") - raise AssertionError("external process must not start") - - monkeypatch.setattr(gateway, "_run", unexpected_run) - exit_intent = replace( - _equity_intent(quantity=1.0), - side="sell", - notional_usd=0.0, - ) - - with pytest.raises(RobinhoodGatewayError, match="kill became active"): - gateway.place_order( - exit_intent, - "a" * 64, - pre_submit_guard=lambda: (_ for _ in ()).throw( - RobinhoodGatewayError("kill became active") - ), - ) - with pytest.raises( - RobinhoodGatewayError, - match="insufficient remaining validity", - ): - gateway.place_order( - exit_intent, - "a" * 64, - pre_submit_guard=lambda: datetime.now(timezone.utc) + timedelta(seconds=35), - ) - - assert run_calls == [] - - -def test_strict_operator_rejects_execute_without_full_authority( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr( - sys, - "argv", - ["operator_live_branch", "--dumbmoney-strict", "--execute"], - ) - - with pytest.raises(SystemExit) as error: - operator_live_branch.main() - - assert error.value.code == 2 - - -def test_operator_rejects_plain_execute_before_gateway_or_model_creation( - monkeypatch: pytest.MonkeyPatch, -) -> None: - gateway_calls: list[bool] = [] - monkeypatch.setattr( - sys, - "argv", - ["operator_live_branch", "--execute"], - ) - monkeypatch.setattr( - operator_live_branch, - "default_gateway", - lambda: gateway_calls.append(True), - ) - - with pytest.raises(SystemExit) as error: - operator_live_branch.main() - - assert error.value.code == 2 - assert gateway_calls == [] - - -def test_programmatic_legacy_execute_rejected_before_gateway_creation( - monkeypatch: pytest.MonkeyPatch, -) -> None: - gateway_calls: list[bool] = [] - monkeypatch.setattr( - operator_live_branch, - "default_gateway", - lambda: gateway_calls.append(True), - ) - - with pytest.raises(RuntimeError, match="Legacy operator execution is disabled"): - operator_live_branch._run_with_crash_tombstone(True) - - assert gateway_calls == [] - - -def test_cancel_result_is_bound_to_exact_account_and_order() -> None: - call = McpToolCallEvidence( - call_id="cancel-1", - tool="cancel_equity_order", - arguments={"account_id": "agentic-account", "order_id": "order-1"}, - result={ - "structured_content": { - "order_id": "order-1", - "status": "cancel_requested", - } - }, - ) - result = _gateway_cancel_result( - { - "cancel_requested": True, - "order_id": "order-1", - "status": "cancel_requested", - "warnings": [], - "error": "", - }, - "cancel_equity_order", - "order-1", - (call,), - "agentic-account", - ) - assert result.cancel_requested is True - - with pytest.raises( - RobinhoodCancellationAmbiguousError, - match="exact Agentic order", - ): - _gateway_cancel_result( - { - "cancel_requested": True, - "order_id": "order-1", - "status": "cancel_requested", - "warnings": [], - "error": "", - }, - "cancel_equity_order", - "order-1", - ( - McpToolCallEvidence( - call_id="cancel-2", - tool="cancel_equity_order", - arguments={ - "account_id": "other-account", - "order_id": "order-1", - }, - result=call.result, - ), - ), - "agentic-account", - ) - - -class CancelGateway: - def __init__(self) -> None: - self.cancel_calls: list[tuple[str, str]] = [] - self.polls = 0 - - def cancel_order(self, instrument: str, order_id: str) -> GatewayCancelResult: - self.cancel_calls.append((instrument, order_id)) - return GatewayCancelResult( - cancel_requested=True, - order_id=order_id, - status="cancel_requested", - warnings=(), - error="", - observed_tools=("cancel_equity_order",), - ) - - def get_account_snapshot(self) -> AccountSnapshot: - self.polls += 1 - return _account( - orders=( - { - "order_id": "order-1", - "symbol": "ACME", - "side": "buy", - "status": "canceled", - "market_date": "2099-01-01", - "submitted_at": "2099-01-01T00:00:30+00:00", - "filled_at": "", - "filled_quantity": 0.0, - "average_fill_price": 0.0, - }, - ) - ) - - -def test_kill_cancel_workflow_is_bounded_and_reconciles_history(tmp_path: Path) -> None: - intent = _equity_intent() - append_record( - ledger_path(tmp_path), - "live_order_result", - { - "intent_id": intent.intent_id, - "instrument": "equity", - "symbol": "ACME", - "side": "buy", - "market_date": "2099-01-01", - "intent": intent.to_dict(), - "placed": True, - "order_id": "order-1", - "status": "open", - "filled_quantity": 0.0, - "average_fill_price": 0.0, - }, - ) - open_account = _account( - orders=( - { - "order_id": "order-1", - "symbol": "ACME", - "side": "buy", - "status": "open", - "market_date": "2099-01-01", - "submitted_at": "2099-01-01T00:00:30+00:00", - "filled_at": "", - "filled_quantity": 0.0, - "average_fill_price": 0.0, - }, - ) - ) - gateway = CancelGateway() - - result = _run_cancel_only_degraded( - tmp_path, - gateway, # type: ignore[arg-type] - open_account, - NOW, - "dumbmoney_kill_active", - True, - ) - - assert result["cycle_status"] == "cancel_only_degraded" - assert gateway.cancel_calls == [("equity", "order-1")] - assert gateway.polls == 1 - assert result["unresolved_attributed_orders"] == [] - - -class HistoryOnlyGateway: - def __init__(self, account: AccountSnapshot) -> None: - self.account = account - self.poll_count = 0 - - def get_account_snapshot(self) -> AccountSnapshot: - self.poll_count += 1 - return self.account - - -def test_ambiguous_placement_polls_history_without_retrying_placement( - tmp_path: Path, -) -> None: - intent = _equity_intent() - broker_order = { - "order_id": "broker-1", - "symbol": "ACME", - "side": "buy", - "status": "open", - "market_date": "2099-01-01", - "submitted_at": "2099-01-01T00:00:30+00:00", - "filled_at": "", - "filled_quantity": 0.0, - "average_fill_price": 0.0, - "order_type": "limit", - "time_in_force": "day", - "limit_price": 100.0, - "quantity": 0.01, - } - gateway = HistoryOnlyGateway(_account(orders=(broker_order,))) - - reconciled, _, poll_count = _poll_ambiguous_placement_history( - tmp_path, - gateway, # type: ignore[arg-type] - intent, - "2099-01-01", - NOW, - "paper-hash", - ) - - assert poll_count == 3 - assert gateway.poll_count == 3 - assert reconciled is not None - assert reconciled["order_id"] == "broker-1" - assert reconciled["placement_retried"] is False - assert any( - record["event_type"] == "live_order_result" - for record in read_ledger(ledger_path(tmp_path)) - ) - - -def test_option_entry_requires_fresh_quote_multiplier_expiry_and_exact_debit() -> None: - intent = LiveExecutionIntent( - **{ - **_equity_intent(notional_usd=125.0, quantity=1.0).to_dict(), - "instrument": "option", - "limit_price": 1.25, - "option_contract": { - "contract_id": "option-1", - "expiry": "2099-01-15", - "multiplier": 100, - "maximum_loss_usd": 125.0, - "quote_observed_at": "2099-01-01T00:00:00Z", - "defined_risk": True, - "order_action": "buy_to_open", - }, - } - ) - validate_option_entry_for_execution(intent, NOW) - - with pytest.raises(LiveSignalError, match="multiplier"): - validate_option_entry_for_execution( - LiveExecutionIntent( - **{ - **intent.to_dict(), - "option_contract": {**intent.option_contract, "multiplier": 10}, - } - ), - NOW, - ) - with pytest.raises(LiveSignalError, match="freshness"): - validate_option_entry_for_execution( - intent, - datetime(2099, 1, 1, 0, 2, tzinfo=timezone.utc), - ) diff --git a/tests/test_dumbmoney_windows_service.py b/tests/test_dumbmoney_windows_service.py deleted file mode 100644 index 8f1c61d..0000000 --- a/tests/test_dumbmoney_windows_service.py +++ /dev/null @@ -1,770 +0,0 @@ -from __future__ import annotations - -import base64 -import hashlib -import json -import multiprocessing -import os -import socket -import sys -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import Any - -import pytest -from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey - -import dopey_live.dumbmoney_windows_service as windows_service -from dopey_live.dumbmoney_authority import ( - _verify_signed_wrapper, - canonical_json_bytes, -) -from dopey_live.dumbmoney_command_feed import ( - CommandFeedHttpResponse, - DumbMoneyCommandDispatch, -) -from dopey_live.dumbmoney_journal_anchor import JournalAnchorError -from dopey_live.dumbmoney_windows_service import ( - CELL_TOKEN_TARGET_REF, - CONFIG_RELATIVE_PATH, - CORE_ENDPOINT_REF, - CORE_READINESS_SCHEMA, - READINESS_REF, - ROBINHOOD_PROFILE_TARGET_REF, - START_MODE, - CoreEndpoint, - DopeyRunnerConfig, - DumbMoneyDopeyWindowsService, - DumbMoneyWindowsServiceError, - PinnedOllamaModelVerifier, - _core_endpoint_from_readiness, - _DataRootLease, - _readiness_signer, - _windows_loopback_listener_owner, - load_runner_config, - load_secrets, -) -from dopey_live.robinhood_gateway import ( - RobinhoodAgenticGateway, - RobinhoodGatewayError, - _codex_exec_command, -) - -NOW = datetime(2099, 1, 1, 0, 0, 30, tzinfo=timezone.utc) -CELL_TOKEN = "cell-token-from-windows-credential-manager" -PROFILE = "robinhood-profile-from-windows-credential-manager" -OLLAMA_SID = "S-1-5-21-1-2-3-1001" - - -def _probe_data_root_lease(path: str, queue: Any) -> None: - try: - with _DataRootLease(Path(path)): - queue.put("acquired") - except DumbMoneyWindowsServiceError: - queue.put("blocked") - - -def _format_utc(value: datetime) -> str: - return ( - value.astimezone(timezone.utc) - .isoformat(timespec="seconds") - .replace("+00:00", "Z") - ) - - -def _key_entry( - private_key: Ed25519PrivateKey, -) -> tuple[str, str, bytes]: - public_key = private_key.public_key().public_bytes_raw() - key_id = hashlib.sha256(public_key).hexdigest() - encoded = base64.urlsafe_b64encode(public_key).decode().rstrip("=") - return key_id, encoded, public_key - - -def _config_payload( - program_data: Path, - core_key: Ed25519PrivateKey, -) -> dict[str, Any]: - core_id, core_encoded, _ = _key_entry(core_key) - del core_id - operator_id, operator_encoded, _ = _key_entry(Ed25519PrivateKey.generate()) - promoter_id, promoter_encoded, _ = _key_entry(Ed25519PrivateKey.generate()) - research_id, research_encoded, _ = _key_entry(Ed25519PrivateKey.generate()) - evaluator_id, evaluator_encoded, _ = _key_entry(Ed25519PrivateKey.generate()) - readiness_public = _readiness_signer(CELL_TOKEN).public_key().public_bytes_raw() - executable_path = Path(sys.executable).resolve() - return { - "schema": "dopey.dumbmoney-robinhood-runner-config.v1", - "service_name": "DumbMoneyDopeyRobinhood", - "release_id": "dumbmoney-test-release", - "core_endpoint_ref": CORE_ENDPOINT_REF, - "core_readiness_path": str( - program_data / "DumbMoney" / "readiness" / "DumbMoneyCore.json" - ), - "core_cell_token_target": CELL_TOKEN_TARGET_REF, - "robinhood_profile_target": ROBINHOOD_PROFILE_TARGET_REF, - "start_mode": START_MODE, - "data_root": str(program_data / "DumbMoney" / "dopey-robinhood"), - "readiness_ref": READINESS_REF, - "readiness_path": str( - program_data / "DumbMoney" / "readiness" / "DumbMoneyDopeyRobinhood.json" - ), - "core_public_key_base64url": core_encoded, - "operator_public_keys_base64url": { - operator_id: operator_encoded, - }, - "promoter_public_keys_base64url": { - promoter_id: promoter_encoded, - }, - "research_public_keys_base64url": { - research_id: research_encoded, - }, - "evaluator_public_keys_base64url": { - evaluator_id: evaluator_encoded, - }, - "expected_account_hash": "c" * 64, - "fund_lock_sha256": "d" * 64, - "service_manifest_sha256": "e" * 64, - "role_public_keys_sha256": "f" * 64, - "core_runner_config_sha256": "1" * 64, - "risk_policy_sha256": "2" * 64, - "readiness_signer_public_key_sha256": hashlib.sha256( - readiness_public - ).hexdigest(), - "codex_executable_path": str(executable_path), - "codex_executable_sha256": hashlib.sha256( - executable_path.read_bytes() - ).hexdigest(), - "codex_local_provider": "ollama", - "codex_local_model": "gpt-oss:20b", - "codex_local_model_digest": "3" * 64, - "ollama_executable_path": str(executable_path), - "ollama_executable_sha256": hashlib.sha256( - executable_path.read_bytes() - ).hexdigest(), - "ollama_process_identity_sid": OLLAMA_SID, - "ollama_runtime_evidence_sha256": "4" * 64, - "poll_interval_seconds": 15, - "readiness_ttl_seconds": 90, - } - - -def _write_config( - program_data: Path, - core_key: Ed25519PrivateKey, -) -> tuple[DopeyRunnerConfig, dict[str, Any]]: - payload = _config_payload(program_data, core_key) - path = program_data / CONFIG_RELATIVE_PATH - path.parent.mkdir(parents=True) - raw = canonical_json_bytes(payload) + b"\n" - path.write_bytes(raw) - digest = hashlib.sha256(raw).hexdigest() - return load_runner_config(path, digest, program_data), payload - - -def _signed_core_readiness( - config: DopeyRunnerConfig, - core_key: Ed25519PrivateKey, -) -> dict[str, Any]: - body = { - "schema": CORE_READINESS_SCHEMA, - "service_name": "DumbMoneyCore", - "release_id": config.release_id, - "instance_id": "8c2df19e-a2dc-4ad4-9b37-c3f03b6c02c6", - "process_id": 42, - "generation": 0, - "observed_at": _format_utc(NOW - timedelta(seconds=5)), - "valid_until": _format_utc(NOW + timedelta(seconds=85)), - "endpoint": { - "transport": "http", - "host": "127.0.0.1", - "port": 49152, - "base_path": "/", - }, - "fund_lock_sha256": config.fund_lock_sha256, - "service_manifest_sha256": config.service_manifest_sha256, - "authority": { - "broker": "NONE", - "mode": "OFFLINE", - "execution_enabled": False, - }, - "health": { - "status": "READY", - "core_public_key_sha256": hashlib.sha256( - config.core_public_key - ).hexdigest(), - "role_public_keys_sha256": config.role_public_keys_sha256, - "runner_config_sha256": config.core_runner_config_sha256, - "risk_policy_sha256": config.risk_policy_sha256, - }, - "capabilities": ["cell-command-checkpoints", "event-ledger"], - } - unsigned: dict[str, Any] = { - "schema": "dumbmoney.signed-envelope.v1", - "source_id": "DumbMoneyCore", - "source_sequence": 1, - "correlation_id": "core-readiness-test", - "causation_id": None, - "nonce": "core-readiness-nonce-1", - "not_before": body["observed_at"], - "expires_at": body["valid_until"], - "body_schema": CORE_READINESS_SCHEMA, - "body_digest": hashlib.sha256(canonical_json_bytes(body)).hexdigest(), - "body": body, - "signature_algorithm": "Ed25519", - "signer_key_id": hashlib.sha256(config.core_public_key).hexdigest(), - } - unsigned["event_id"] = hashlib.sha256(canonical_json_bytes(unsigned)).hexdigest() - signature = core_key.sign(canonical_json_bytes(unsigned)) - return { - **unsigned, - "signature": base64.urlsafe_b64encode(signature).decode().rstrip("="), - } - - -def test_public_config_is_exact_path_and_exact_raw_sha( - tmp_path: Path, -) -> None: - core_key = Ed25519PrivateKey.generate() - config, payload = _write_config(tmp_path, core_key) - - assert config.expected_account_hash == "c" * 64 - assert config.data_root == (tmp_path / "DumbMoney" / "dopey-robinhood").resolve() - - path = tmp_path / CONFIG_RELATIVE_PATH - path.write_bytes(canonical_json_bytes({**payload, "release_id": "tampered"})) - with pytest.raises( - DumbMoneyWindowsServiceError, - match="does not match --config-sha256", - ): - load_runner_config(path, config.config_sha256, tmp_path) - - -class FakeCredentials: - def __init__(self) -> None: - self.targets: list[str] = [] - - def read_bytes(self, target: str) -> bytes: - self.targets.append(target) - if target == "DumbMoney/DopeyCellToken": - return CELL_TOKEN.encode() - if target == "DumbMoney/RobinhoodProfile": - return PROFILE.encode() - raise AssertionError("unexpected credential target") - - -def test_secrets_load_only_from_two_fixed_credential_targets( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setenv("DUMBMONEY_DOPEY_CELL_TOKEN", "forbidden-env-token") - monkeypatch.setenv("ROBINHOOD_PROFILE", "forbidden-env-profile") - provider = FakeCredentials() - - assert load_secrets(provider) == (CELL_TOKEN, PROFILE) - assert provider.targets == [ - "DumbMoney/DopeyCellToken", - "DumbMoney/RobinhoodProfile", - ] - - -def test_core_endpoint_requires_current_signed_pinned_readiness( - tmp_path: Path, -) -> None: - core_key = Ed25519PrivateKey.generate() - config, _ = _write_config(tmp_path, core_key) - readiness = _signed_core_readiness(config, core_key) - config.core_readiness_path.parent.mkdir(parents=True, exist_ok=True) - config.core_readiness_path.write_bytes(canonical_json_bytes(readiness)) - - endpoint = _core_endpoint_from_readiness(config, NOW) - - assert endpoint.base_url == "http://127.0.0.1:49152" - tampered = json.loads(json.dumps(readiness)) - tampered["body"]["endpoint"]["host"] = "example.com" - config.core_readiness_path.write_bytes(canonical_json_bytes(tampered)) - with pytest.raises( - DumbMoneyWindowsServiceError, - match="body digest mismatch", - ): - _core_endpoint_from_readiness(config, NOW) - config.core_readiness_path.write_bytes(canonical_json_bytes(readiness)) - with pytest.raises( - DumbMoneyWindowsServiceError, - match="not currently valid", - ): - _core_endpoint_from_readiness( - config, - NOW + timedelta(minutes=2), - ) - - -class FakeCommandClient: - dispatch: DumbMoneyCommandDispatch - - def __init__(self, **_kwargs: Any) -> None: - pass - - def poll_once(self, _now: datetime) -> DumbMoneyCommandDispatch: - return self.dispatch - - -class FakeResolver: - def __init__(self, **_kwargs: Any) -> None: - pass - - -class FakeJournalAnchor: - def __init__(self, *, reject: bool = False) -> None: - self.reject = reject - self.calls: list[dict[str, Any]] = [] - - def anchor(self, **kwargs: Any) -> None: - self.calls.append(dict(kwargs)) - if self.reject: - raise JournalAnchorError("Core rejected rolled-back journal") - - -def test_service_starts_reconciliation_only_then_allows_next_cycle( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - core_key = Ed25519PrivateKey.generate() - config, _ = _write_config(tmp_path, core_key) - FakeCommandClient.dispatch = DumbMoneyCommandDispatch( - desired_mode="LIVE", - kill_active=False, - allow_new_risk=True, - capital_authority=None, - fail_closed_reason="", - required_action="APPLY_SIGNED_CONTROLS", - caught_up=True, - next_sequence=7, - next_digest="7" * 64, - applied_event_ids=(), - ) - monkeypatch.setattr( - windows_service, - "_core_endpoint_from_readiness", - lambda *_args: CoreEndpoint( - "http://127.0.0.1:49152", - "8c2df19e-a2dc-4ad4-9b37-c3f03b6c02c6", - _format_utc(NOW), - _format_utc(NOW + timedelta(seconds=90)), - ), - ) - monkeypatch.setattr( - windows_service, - "_core_liveness", - lambda *_args: None, - ) - monkeypatch.setattr( - windows_service, - "DumbMoneyCommandFeedClient", - FakeCommandClient, - ) - monkeypatch.setattr( - windows_service, - "DumbMoneyContractResolverClient", - FakeResolver, - ) - execute_values: list[bool] = [] - anchor_client = FakeJournalAnchor() - - def cycle_runner( - _root: Path, - _gateway: object, - _now: datetime, - execute_orders: bool, - **_kwargs: Any, - ) -> dict[str, Any]: - execute_values.append(execute_orders) - return { - "cycle_status": "complete" if execute_orders else "preflight_complete", - "order_history_blocker": "", - "account": { - "authenticated": True, - "account_hash": config.expected_account_hash, - }, - "orders": [], - } - - service = DumbMoneyDopeyWindowsService( - config, - CELL_TOKEN, - PROFILE, - transport=lambda _request: (_ for _ in ()).throw( - AssertionError("transport was monkeypatched away") - ), - gateway=object(), # type: ignore[arg-type] - cycle_runner=cycle_runner, - clock=lambda: NOW, - journal_anchor_factory=lambda _base_url: anchor_client, # type: ignore[arg-type] - ) - try: - first = service.run_once() - second = service.run_once() - readiness = json.loads(config.readiness_path.read_text()) - verified = _verify_signed_wrapper( - readiness, - _readiness_signer(CELL_TOKEN).public_key().public_bytes_raw(), - NOW, - enforce_current_window=True, - ) - finally: - service._health_server.server.server_close() - - assert first["cycle_status"] == "preflight_complete" - assert second["cycle_status"] == "complete" - assert execute_values == [False, True] - assert len(anchor_client.calls) == 18 - assert service.state().mode == "AGGRESSIVE_BOUNDED" - assert service.state().rollback_anchor_current is True - assert verified.body["authority"]["execution_enabled"] is True - assert verified.body["health"]["command_sequence"] == 7 - assert verified.body["health"]["rollback_anchor_current"] is True - - -def test_rollback_anchor_rejection_blocks_before_broker_contact( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - core_key = Ed25519PrivateKey.generate() - config, _ = _write_config(tmp_path, core_key) - monkeypatch.setattr( - windows_service, - "_core_endpoint_from_readiness", - lambda *_args: CoreEndpoint( - "http://127.0.0.1:49152", - "8c2df19e-a2dc-4ad4-9b37-c3f03b6c02c6", - _format_utc(NOW), - _format_utc(NOW + timedelta(seconds=90)), - ), - ) - monkeypatch.setattr( - windows_service, - "_core_liveness", - lambda *_args: None, - ) - cycle_calls: list[bool] = [] - anchor_client = FakeJournalAnchor(reject=True) - service = DumbMoneyDopeyWindowsService( - config, - CELL_TOKEN, - PROFILE, - transport=lambda _request: (_ for _ in ()).throw( - AssertionError("command transport must not run") - ), - gateway=object(), # type: ignore[arg-type] - cycle_runner=lambda *_args, **_kwargs: cycle_calls.append(True), # type: ignore[arg-type] - clock=lambda: NOW, - journal_anchor_factory=lambda _base_url: anchor_client, # type: ignore[arg-type] - ) - try: - result = service.run_once() - readiness = json.loads(config.readiness_path.read_text()) - finally: - service._health_server.server.server_close() - - assert result == { - "cycle_status": "blocked_rollback_anchor", - "orders": [], - "broker_contacted": False, - } - assert cycle_calls == [] - assert service.state().health_status == "BLOCKED" - assert service.state().rollback_anchor_current is False - assert readiness["body"]["authority"]["execution_enabled"] is False - assert readiness["body"]["health"]["rollback_anchor_current"] is False - - -def test_corrupt_local_journal_blocks_before_broker_contact( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - core_key = Ed25519PrivateKey.generate() - config, _ = _write_config(tmp_path, core_key) - command_state = config.data_root / "control" / "command-feed-state.v1.json" - command_state.parent.mkdir(parents=True) - command_state.write_text("{not-json", encoding="utf-8") - monkeypatch.setattr( - windows_service, - "_core_endpoint_from_readiness", - lambda *_args: CoreEndpoint( - "http://127.0.0.1:49152", - "8c2df19e-a2dc-4ad4-9b37-c3f03b6c02c6", - _format_utc(NOW), - _format_utc(NOW + timedelta(seconds=90)), - ), - ) - monkeypatch.setattr( - windows_service, - "_core_liveness", - lambda *_args: None, - ) - cycle_calls: list[bool] = [] - anchor_client = FakeJournalAnchor() - service = DumbMoneyDopeyWindowsService( - config, - CELL_TOKEN, - PROFILE, - transport=lambda _request: (_ for _ in ()).throw( - AssertionError("command transport must not run") - ), - gateway=object(), # type: ignore[arg-type] - cycle_runner=lambda *_args, **_kwargs: cycle_calls.append(True), # type: ignore[arg-type] - clock=lambda: NOW, - journal_anchor_factory=lambda _base_url: anchor_client, # type: ignore[arg-type] - ) - try: - result = service.run_once() - finally: - service._health_server.server.server_close() - - assert result["cycle_status"] == "blocked_rollback_anchor" - assert result["broker_contacted"] is False - assert cycle_calls == [] - assert anchor_client.calls == [] - - -def test_service_startup_writes_readiness_without_broker_call( - tmp_path: Path, -) -> None: - core_key = Ed25519PrivateKey.generate() - config, _ = _write_config(tmp_path, core_key) - cycle_calls: list[bool] = [] - service = DumbMoneyDopeyWindowsService( - config, - CELL_TOKEN, - PROFILE, - transport=lambda _request: (_ for _ in ()).throw( - AssertionError("startup must not call Core") - ), - gateway=object(), # type: ignore[arg-type] - cycle_runner=lambda *_args, **_kwargs: cycle_calls.append(True), # type: ignore[arg-type] - clock=lambda: NOW, - ) - - service.start() - service.close() - - assert cycle_calls == [] - readiness = json.loads(config.readiness_path.read_text()) - assert readiness["body"]["authority"] == { - "broker": "ROBINHOOD", - "mode": "RECONCILIATION_ONLY", - "execution_enabled": False, - } - - -def test_profile_is_in_memory_only_and_child_executable_is_sha_pinned( - tmp_path: Path, -) -> None: - with pytest.raises(ValueError, match="prohibited"): - RobinhoodAgenticGateway( - lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError("test must not spawn a process") - ), - 60, - mcp_bearer_token=PROFILE, - ) - - executable = tmp_path / "codex.exe" - executable.write_bytes(b"sealed-codex-fixture") - executable_sha256 = hashlib.sha256(executable.read_bytes()).hexdigest() - with pytest.raises(ValueError, match="prohibited"): - RobinhoodAgenticGateway( - lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError("test must not spawn a process") - ), - 60, - mcp_bearer_token=PROFILE, - codex_executable_path=executable, - codex_executable_sha256=executable_sha256, - ) - with pytest.raises(ValueError, match="prohibited"): - _codex_exec_command( - str(executable), - str(tmp_path), - tmp_path / "result.json", - ("get_accounts",), - "DUMBMONEY_ROBINHOOD_MCP_BEARER", - ) - gateway = RobinhoodAgenticGateway( - lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError("test must not spawn a process") - ), - 60, - temporary_root=tmp_path / "temp", - codex_executable_path=executable, - codex_executable_sha256=executable_sha256, - local_model="gpt-oss:20b", - local_model_verifier=lambda: None, - expected_account_hash="a" * 64, - deterministic_mcp_call=( - lambda _tool, _arguments, _timeout, _pre_call_guard: {} - ), - ) - command = _codex_exec_command( - str(executable), - str(tmp_path), - tmp_path / "result.json", - ("get_accounts",), - None, - "gpt-oss:20b", - ) - - joined_command = " ".join(command) - assert PROFILE not in joined_command - assert command[command.index("--local-provider") + 1] == "ollama" - assert command[command.index("--model") + 1] == "gpt-oss:20b" - assert "--oss" in command - assert not any("openai" in value.lower() for value in command) - assert "mcp_servers." not in joined_command - assert "agent.robinhood.com" not in joined_command - assert "DUMBMONEY_ROBINHOOD_MCP_BEARER" not in joined_command - assert gateway._subprocess_env is None - assert gateway._bearer_token_env_var is None - - executable.write_bytes(b"path-hijack") - with pytest.raises(RobinhoodGatewayError, match="SHA-256 mismatch"): - gateway._verify_codex_executable() - - -def test_pinned_ollama_model_is_verified_by_exact_local_inventory_digest( - tmp_path: Path, -) -> None: - calls: list[Any] = [] - executable_path = Path(sys.executable).resolve() - executable_sha256 = hashlib.sha256(executable_path.read_bytes()).hexdigest() - evidence_path = tmp_path / "ollama-runtime-evidence.v1.json" - evidence_path.write_bytes(b'{"evidence_kind":"STATIC_INSTALLATION_ATTESTATION"}\n') - evidence_sha256 = hashlib.sha256(evidence_path.read_bytes()).hexdigest() - owner_calls: list[tuple[str, int]] = [] - - def transport(request: Any) -> CommandFeedHttpResponse: - calls.append(request) - return CommandFeedHttpResponse( - status=200, - headers={"Content-Type": "application/json"}, - body=canonical_json_bytes( - { - "models": [ - { - "name": "gpt-oss:20b", - "model": "gpt-oss:20b", - "digest": "3" * 64, - } - ] - } - ), - ) - - def listener_owner(host: str, port: int) -> tuple[int, Path, str]: - owner_calls.append((host, port)) - return 42, executable_path, OLLAMA_SID - - verifier = PinnedOllamaModelVerifier( - "gpt-oss:20b", - "3" * 64, - executable_path, - executable_sha256, - OLLAMA_SID, - evidence_path, - evidence_sha256, - transport, - listener_owner, - ) - verifier() - - assert len(calls) == 1 - assert owner_calls == [ - ("127.0.0.1", 11_434), - ("127.0.0.1", 11_434), - ] - assert calls[0].url == "http://127.0.0.1:11434/api/tags" - assert calls[0].method == "GET" - - wrong = PinnedOllamaModelVerifier( - "gpt-oss:20b", - "4" * 64, - executable_path, - executable_sha256, - OLLAMA_SID, - evidence_path, - evidence_sha256, - transport, - listener_owner, - ) - with pytest.raises( - DumbMoneyWindowsServiceError, - match="model digest mismatch", - ): - wrong() - - wrong_owner = PinnedOllamaModelVerifier( - "gpt-oss:20b", - "3" * 64, - executable_path, - executable_sha256, - OLLAMA_SID, - evidence_path, - evidence_sha256, - transport, - lambda _host, _port: ( - 42, - executable_path.with_name("not-ollama.exe"), - OLLAMA_SID, - ), - ) - with pytest.raises( - DumbMoneyWindowsServiceError, - match="not owned by the pinned identity and executable", - ): - wrong_owner() - - -@pytest.mark.skipif(os.name != "nt", reason="Windows TCP owner API") -def test_windows_tcp_owner_resolves_exact_loopback_listener_process() -> None: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener: - listener.bind(("127.0.0.1", 0)) - listener.listen() - port = int(listener.getsockname()[1]) - - process_id, image_path, process_sid = _windows_loopback_listener_owner( - "127.0.0.1", - port, - ) - - assert process_id == os.getpid() - assert image_path.is_file() - assert image_path.name.casefold() == "python.exe" - assert process_sid.startswith("S-1-5-") - - -@pytest.mark.skipif(os.name != "nt", reason="Windows byte-range process lock") -def test_data_root_lease_blocks_concurrent_writer_and_releases_after_exit( - tmp_path: Path, -) -> None: - lock_path = tmp_path / "service.lock" - context = multiprocessing.get_context("spawn") - - with _DataRootLease(lock_path): - blocked_queue = context.Queue() - blocked = context.Process( - target=_probe_data_root_lease, - args=(str(lock_path), blocked_queue), - ) - blocked.start() - blocked.join(10) - assert blocked.exitcode == 0 - assert blocked_queue.get(timeout=2) == "blocked" - - acquired_queue = context.Queue() - acquired = context.Process( - target=_probe_data_root_lease, - args=(str(lock_path), acquired_queue), - ) - acquired.start() - acquired.join(10) - assert acquired.exitcode == 0 - assert acquired_queue.get(timeout=2) == "acquired" diff --git a/tests/test_no_live_artifact_bindings.py b/tests/test_no_live_artifact_bindings.py index 3c8b3b3..b107fe6 100644 --- a/tests/test_no_live_artifact_bindings.py +++ b/tests/test_no_live_artifact_bindings.py @@ -18,7 +18,7 @@ import sys from pathlib import Path -from tests import conftest +import conftest # Vendored third-party trees and PowerShell-only dirs: nothing to import. _WALK_EXCLUDED_TOP_LEVEL = {"dopey_open_source_vendor"} diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 09fd8ac..0000000 --- a/uv.lock +++ /dev/null @@ -1,1309 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.11" -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] - -[[package]] -name = "beautifulsoup4" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, -] - -[[package]] -name = "bottle" -version = "0.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7a/71/cca6167c06d00c81375fd668719df245864076d284f7cb46a694cbeb5454/bottle-0.13.4.tar.gz", hash = "sha256:787e78327e12b227938de02248333d788cfe45987edca735f8f88e03472c3f47", size = 98717, upload-time = "2025-06-15T10:08:59.439Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/f6/b55ec74cfe68c6584163faa311503c20b0da4c09883a41e8e00d6726c954/bottle-0.13.4-py2.py3-none-any.whl", hash = "sha256:045684fbd2764eac9cdeb824861d1551d113e8b683d8d26e296898d3dd99a12e", size = 103807, upload-time = "2025-06-15T10:08:57.691Z" }, -] - -[[package]] -name = "certifi" -version = "2026.7.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, -] - -[[package]] -name = "cffi" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, - { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, - { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, - { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, - { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, - { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, - { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, - { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, - { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, - { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, - { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, - { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, - { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, - { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, - { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, - { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, - { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, - { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, - { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, - { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, - { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, - { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, - { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, - { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, - { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, - { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, - { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, - { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, - { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, - { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, - { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, - { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, - { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, - { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, - { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, - { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, - { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, - { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, - { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, - { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, - { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, - { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, - { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, - { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, - { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, - { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, - { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, - { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, - { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, - { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, - { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, - { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, - { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, - { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, - { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, - { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, - { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, - { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/e3/85ec501f206fb049259288c1f3506e53876937fb00edb47009348e66756b/charset_normalizer-3.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5", size = 317075, upload-time = "2026-07-07T14:32:56.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/69/2a5385192e67175f7d8bd5ce4f57c24bc956439adeae5c13a99aa28a53d1/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2", size = 213837, upload-time = "2026-07-07T14:32:57.78Z" }, - { url = "https://files.pythonhosted.org/packages/b3/46/03ddc7da576d814fe0a36dd1f0fd3258e95404b4b2e3c026b7923d7e133f/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a", size = 235503, upload-time = "2026-07-07T14:32:59.205Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6e/de0229a7ef40f6f9d28a837eebf4ec47bdca5dab4e900c84f22919af636a/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29", size = 229944, upload-time = "2026-07-07T14:33:00.803Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/49b9060e8418b14fb5cba9cf6bfb383111e2538a03a1fb18e66a95aeb3d5/charset_normalizer-3.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c", size = 221276, upload-time = "2026-07-07T14:33:02.199Z" }, - { url = "https://files.pythonhosted.org/packages/44/95/80282cce0fae9c3061203d723ee87da996aed79679e65d8935050ee7ca1f/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b", size = 205260, upload-time = "2026-07-07T14:33:03.698Z" }, - { url = "https://files.pythonhosted.org/packages/0c/74/2f62c8821b969ea3bd67cc2e6976834f48ca5d12664d2559ebcd9bcfbed7/charset_normalizer-3.4.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db", size = 217786, upload-time = "2026-07-07T14:33:05.12Z" }, - { url = "https://files.pythonhosted.org/packages/d9/8d/feabb82cb49fcad14515b1d7d1ca4787b0da7fc723a212bf89bc9e0fac52/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993", size = 216798, upload-time = "2026-07-07T14:33:06.629Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ff/c946d63bc3786d5b84d960b0f7ab7e25b828486a946b5aa997625bcaf6a6/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da", size = 206429, upload-time = "2026-07-07T14:33:08.006Z" }, - { url = "https://files.pythonhosted.org/packages/af/ba/5e5007c370702f85d2ef75791fac7943ed41e080364a673b20142e430e3e/charset_normalizer-3.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3", size = 223066, upload-time = "2026-07-07T14:33:09.783Z" }, - { url = "https://files.pythonhosted.org/packages/83/d5/9096aa3cf532dfad237861544eb47a0f20d5adbf1039760fed8eaae935d9/charset_normalizer-3.4.9-cp311-cp311-win32.whl", hash = "sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d", size = 150456, upload-time = "2026-07-07T14:33:11.217Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a1/e29995109e455dc8eff8d0fac6ae509be39561318a7cfeac5d33ad029213/charset_normalizer-3.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1", size = 161410, upload-time = "2026-07-07T14:33:12.743Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8d/1569f4d0032d6ba2a4fe4591c35bf87868c600c41a71eb5c2e1ffa8464c2/charset_normalizer-3.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec", size = 152649, upload-time = "2026-07-07T14:33:14.173Z" }, - { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, - { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, - { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, - { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, - { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, - { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, - { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, - { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, - { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, - { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, - { url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" }, - { url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" }, - { url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" }, - { url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" }, - { url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" }, - { url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" }, - { url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" }, - { url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" }, - { url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" }, - { url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" }, - { url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" }, - { url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" }, - { url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" }, - { url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" }, - { url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" }, - { url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" }, - { url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" }, - { url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" }, - { url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" }, - { url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" }, - { url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" }, - { url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" }, - { url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" }, - { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, -] - -[[package]] -name = "clr-loader" -version = "0.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/46/7eea92b6aa2d68af78e049cbecec5f757f1aad44ecdecdc16bbad7eead51/clr_loader-0.3.1.tar.gz", hash = "sha256:2e073e9aaf49d1ae2f56ecba27987ad5fb68be4bcd9dd34a5bed8f0e4e128366", size = 86805, upload-time = "2026-04-18T17:49:44.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/da/ec1a6e36624000b6df0dd61183c42342ee5814c073315e802cadaad04d2f/clr_loader-0.3.1-py3-none-any.whl", hash = "sha256:cbad189de20d202a7d621956b0fc38049e13c9bf7ca2923441eff725cd121aa1", size = 55730, upload-time = "2026-04-18T17:49:42.99Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "cryptography" -version = "49.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, - { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, - { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, - { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, - { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, - { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, - { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, - { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, - { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, - { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, - { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, - { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, - { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, - { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, - { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, - { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, - { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, - { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, - { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, - { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, - { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, - { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, - { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, - { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, - { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, - { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, -] - -[[package]] -name = "curl-cffi" -version = "0.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "cffi" }, - { name = "rich" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/5b/89fcfebd3e5e85134147ac99e9f2b2271165fd4d71984fc65da5f17819b7/curl_cffi-0.15.0.tar.gz", hash = "sha256:ea0c67652bf6893d34ee0f82c944f37e488f6147e9421bef1771cc6545b02ded", size = 196437, upload-time = "2026-04-03T11:12:31.525Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/42/54ddd442c795f30ce5dd4e49f87ce77505958d3777cd96a91567a3975d2a/curl_cffi-0.15.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:bda66404010e9ed743b1b83c20c86f24fe21a9a6873e17479d6e67e29d8ded28", size = 2795267, upload-time = "2026-04-03T11:11:46.48Z" }, - { url = "https://files.pythonhosted.org/packages/83/2d/3915e238579b3c5a92cead5c79130c3b8d20caaba7616cc4d894650e1d6b/curl_cffi-0.15.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a25620d9bf989c9c029a7d1642999c4c265abb0bad811deb2f77b0b5b2b12e5b", size = 2573544, upload-time = "2026-04-03T11:11:47.951Z" }, - { url = "https://files.pythonhosted.org/packages/2a/b3/9d2f1057749a1b07ba1989db3c1503ce8bed998310bae9aea2c43aa64f20/curl_cffi-0.15.0-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:582e570aa2586b96ed47cf4a17586b9a3c462cbe43f780487c3dc245c6ef1527", size = 10515369, upload-time = "2026-04-03T11:11:50.126Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1d/6d10dded5ce3fd8157e558ebd97d09e551b77a62cdc1c31e93d0a633cee5/curl_cffi-0.15.0-cp310-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:838e48212447d9c81364b04707a5c861daf08f8320f9ecb3406a8919d1d5c3b3", size = 10160045, upload-time = "2026-04-03T11:11:52.664Z" }, - { url = "https://files.pythonhosted.org/packages/5c/12/c70b835487ace3b9ba1502631912e3440082b8ae3a162f60b59cb0b6444d/curl_cffi-0.15.0-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b6c847d86283b07ae69bb72c82eb8a59242277142aa35b89850f89e792a02fc", size = 11090433, upload-time = "2026-04-03T11:11:55.049Z" }, - { url = "https://files.pythonhosted.org/packages/ea/0d/78edcc4f71934225db99df68197a107386d59080742fc7bf6bb4d007924f/curl_cffi-0.15.0-cp310-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e5e69eee735f659287e2c84444319d68a1fa68dd37abf228943a4074864283a", size = 10479178, upload-time = "2026-04-03T11:11:57.685Z" }, - { url = "https://files.pythonhosted.org/packages/5b/84/1e101c1acb1ea2f0b4992f5c3024f596d8e21db0d53540b9d583f673c4e7/curl_cffi-0.15.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1323950224db24f4c510d010b3affa02196ca853fb424191fa917a513d3f4b", size = 10317051, upload-time = "2026-04-03T11:12:00.295Z" }, - { url = "https://files.pythonhosted.org/packages/28/42/8ef236b22a6c23d096c85a1dc507efe37bfdfc7a2f8a4b34efb590197369/curl_cffi-0.15.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:41f80170ba844009273b2660da1964ec31e99e5719d16b3422ada87177e32e13", size = 11299660, upload-time = "2026-04-03T11:12:02.791Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/56aeb055d962da87a1be0d74c6c644e251c7e88129b5471dc44ac724e678/curl_cffi-0.15.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1977e1e12cfb5c11352cbb74acef1bed24eb7d226dab61ca57c168c21acd4d61", size = 11945049, upload-time = "2026-04-03T11:12:05.912Z" }, - { url = "https://files.pythonhosted.org/packages/d8/8c/2abf99a38d6340d66cf0557e0c750ef3f8883dfc5d450087e01c85861343/curl_cffi-0.15.0-cp310-abi3-win_amd64.whl", hash = "sha256:5a0c1896a0d5a5ac1eb89cd24b008d2b718dd1df6fd2f75451b59ca66e49e572", size = 1661649, upload-time = "2026-04-03T11:12:07.948Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/dfd54f2240d3a9b96d77bacc62b97813b35e2aa8ecf5cd5013c683f1ba96/curl_cffi-0.15.0-cp310-abi3-win_arm64.whl", hash = "sha256:a6d57f8389273a3a1f94370473c74897467bcc36af0a17336989780c507fa43d", size = 1410741, upload-time = "2026-04-03T11:12:10.073Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/c24df8a4fc22fa84070dcd94abeba43c15e08cc09e35869565c0bad196fd/curl_cffi-0.15.0-cp313-abi3-android_24_arm64_v8a.whl", hash = "sha256:4682dc38d4336e0eb0b185374db90a760efde63cbea994b4e63f3521d44c4c92", size = 7190427, upload-time = "2026-04-03T11:12:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/11/56/132225cb3491d07cc6adcce5fe395e059bde87c68cff1ef87a31c88c7819/curl_cffi-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:967ad7355bd8e9586f8c2d02eaa99953747549e7ea4a9b25cd53353e6b67fe6d", size = 2795723, upload-time = "2026-04-03T11:12:13.668Z" }, - { url = "https://files.pythonhosted.org/packages/07/8f/f4f83cd303bef7e8f1749512e5dd157e7e5d08b0a36c8211f9640a2757bf/curl_cffi-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e63539d0d839d0a8c5eacf86229bc68c57803547f35e0db7ee0986328b478c3", size = 2573739, upload-time = "2026-04-03T11:12:15.08Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5c/643d65c7fc9acd742876aa55c2d7823c438cb7665810acd2e66c9976c4d9/curl_cffi-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08c799b89740b9bc49c09fbc3d5907f13ac1f845ca52620507ef9466d4639dd5", size = 10521046, upload-time = "2026-04-03T11:12:17.034Z" }, - { url = "https://files.pythonhosted.org/packages/7f/0b/9b8037113c93f4c5323096163471fa7c35c7676c3f608eeaf1287cd99d58/curl_cffi-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b7a92767a888ee90147e18964b396d8435ff42737030d6fb00824ffd6094805", size = 11096115, upload-time = "2026-04-03T11:12:19.694Z" }, - { url = "https://files.pythonhosted.org/packages/5f/96/fff2fcbd924ef4042e0d67379f751a8a4e3186a91e75e35a4cf218b306ee/curl_cffi-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:829cc357061ecb99cc2d406301f609a039e05665322f5c025ec67c38b0dc49ce", size = 11305346, upload-time = "2026-04-03T11:12:22.151Z" }, - { url = "https://files.pythonhosted.org/packages/53/1b/304b253a45ab28691c8c5e8cca1e6cbb9cf8e46dfceae4648dd536f75e73/curl_cffi-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:408d6f14e346841cd889c2e0962832bb235ba3b6749ebf609f347f747da5e60f", size = 11949834, upload-time = "2026-04-03T11:12:24.986Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ff/4723d92f08259c707a974aba27a08d0a822b9555e35ca581bf18d055a364/curl_cffi-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b624c7ce087bfda967a013ed0a64702a525444e5b6e97d23534d567ccc6525aa", size = 1702771, upload-time = "2026-04-03T11:12:28.201Z" }, - { url = "https://files.pythonhosted.org/packages/59/8c/36bbe06d66fa2b765e4a07199f643a59a9cd1a754207a96335402a9520f4/curl_cffi-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0b6c0543b993996670e9e4b78e305a2d60809d5681903ffb5568e21a387434d3", size = 1466312, upload-time = "2026-04-03T11:12:30.054Z" }, -] - -[[package]] -name = "dopey" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "cryptography" }, - { name = "exchange-calendars" }, - { name = "requests" }, - { name = "tzdata" }, - { name = "yfinance" }, -] - -[package.optional-dependencies] -desktop = [ - { name = "pystray" }, - { name = "pywebview" }, -] -dev = [ - { name = "pytest" }, - { name = "pyyaml" }, - { name = "ruff" }, -] - -[package.metadata] -requires-dist = [ - { name = "cryptography", specifier = "==49.0.0" }, - { name = "exchange-calendars", specifier = "==4.13.2" }, - { name = "pystray", marker = "extra == 'desktop'", specifier = "==0.19.5" }, - { name = "pytest", marker = "extra == 'dev'", specifier = "==9.1.1" }, - { name = "pywebview", marker = "extra == 'desktop'", specifier = "==6.2.1" }, - { name = "pyyaml", marker = "extra == 'dev'", specifier = "==6.0.3" }, - { name = "requests", specifier = "==2.34.2" }, - { name = "ruff", marker = "extra == 'dev'", specifier = "==0.15.20" }, - { name = "tzdata", specifier = "==2026.2" }, - { name = "yfinance", specifier = "==1.5.1" }, -] -provides-extras = ["desktop", "dev"] - -[[package]] -name = "exchange-calendars" -version = "4.13.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "korean-lunar-calendar" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pandas" }, - { name = "pyluach" }, - { name = "toolz" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/73/460b0ece4e7444e3098b7738974c822787e134578d069d3a88b4be17d50a/exchange_calendars-4.13.2.tar.gz", hash = "sha256:a9459425dd64142cd54fbc639847403c7e0c33d60fbc326c94fc1d6bd127f002", size = 4137321, upload-time = "2026-03-10T03:24:38.536Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/4c/0469b40057bc9f8d9594dcc6024202626b981ae4b52dfcd304552e8e1c3a/exchange_calendars-4.13.2-py3-none-any.whl", hash = "sha256:fc5a2ad0d61b5c3a6539a3061cd4cbb55c59f4a903455cec7926e4b798919996", size = 213306, upload-time = "2026-03-10T03:24:37.055Z" }, -] - -[[package]] -name = "idna" -version = "3.18" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "korean-lunar-calendar" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/9f/6160e95f3934026194dc7a0b5c78fe5c6e74761830ddea3e9cfc38e3f983/korean_lunar_calendar-0.4.0.tar.gz", hash = "sha256:be56f27bc0594fdbbdf7bbe00f504a9f929a31e311bd7d9bb93561b645afade7", size = 13425, upload-time = "2026-06-15T14:19:57.163Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/88/f56033b09fdca5c8c5e5fc2f3513950a05cd62caa9bd8e6d2750a887c86e/korean_lunar_calendar-0.4.0-py3-none-any.whl", hash = "sha256:c042e20de0bb702add6bec8d0f6da1ea8d3b170838e63846f70420cf341fe4e7", size = 11323, upload-time = "2026-06-15T14:19:56.089Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - -[[package]] -name = "multitasking" -version = "0.0.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/c3/ac2cc9307fb15cc28ed6d4a9266b216c83ee7fe64299f0264047982bce88/multitasking-0.0.13.tar.gz", hash = "sha256:d896b5df877c9ca5eeddbf0e5994124694d6cb535aba698fb23344c7025155a1", size = 20585, upload-time = "2026-04-23T12:14:15.049Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/1c/24dbf69b247f287401c904a396233a43c89fd4fb9b7cd2e50e430e9cd57c/multitasking-0.0.13-py3-none-any.whl", hash = "sha256:ec9243af140c67bfe52dc98d7173c294512735a88e8425c458b250db99dc2b48", size = 16380, upload-time = "2026-04-23T12:14:13.776Z" }, -] - -[[package]] -name = "numpy" -version = "2.4.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, - { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, - { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, - { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, - { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, - { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, - { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, - { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, - { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, - { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, - { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, - { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, - { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, - { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, - { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, - { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, - { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, - { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, - { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, - { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, - { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, - { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, - { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, - { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, - { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, - { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, - { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, - { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, - { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, - { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, - { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, - { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, - { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, - { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, - { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, - { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, - { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, - { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, - { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, - { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, - { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, - { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, -] - -[[package]] -name = "numpy" -version = "2.5.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/7b/14687aa674250e5e546f616f486b0d56d3631cd5b2415739141ce40bdcea/numpy-2.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c889b56fe48b1018f764b0eec8df59ab654e9148aa91faa12596043500de277", size = 16801574, upload-time = "2026-07-04T17:06:12.423Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/cc5bb2a3f2913d27d6dbb2c78d25921fabaedc6741d4a5a615a11f3c5bf3/numpy-2.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab451b59c5643c570974c43aef780703ef1d3b4965d2be07afd530615a9358d1", size = 11772250, upload-time = "2026-07-04T17:06:15.726Z" }, - { url = "https://files.pythonhosted.org/packages/42/77/fdf34a71dd30f54979b18603bee915e0aaf825b07afe79acd60b04b691e2/numpy-2.5.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:78798bd5b9ad744056af8efa90e3b9ddaa53272a0848a483084a1cc0a13b2dc0", size = 5331516, upload-time = "2026-07-04T17:06:17.913Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e2/eb7efa015b4cce41e2517bf182a7fce0d7d5b9d9ed76a29bfa0f4fe4505c/numpy-2.5.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:2ae0ca40bcb22d6ba59c1dfd5446f49940b0f2d821fde133f10dda11f816b84e", size = 6664863, upload-time = "2026-07-04T17:06:20.02Z" }, - { url = "https://files.pythonhosted.org/packages/a9/4b/a2b32dd94ee9ffbeecb28152240042a3949db33b1c834d44090b80e1b3b8/numpy-2.5.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61ac47e772e6b8ea489e1d2f441a34c5c3ac17327e7ce294cbdf535795ad4e75", size = 15167977, upload-time = "2026-07-04T17:06:21.621Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/6e73d68500f80773f65f0654ea932019d6694329a0eb0ed0533de38df376/numpy-2.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:59fda5e192b570217ec2580c96f00e9a7e12ef6866a900eb089b62c1a32545ca", size = 16672469, upload-time = "2026-07-04T17:06:24.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/7d/ad3e59015135f5261c95fd4cafeff159c955febd83a99a1d9250c4233815/numpy-2.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f7119ebff1a9829e9f431a4f9d28e703023bb6b9fe7c8f724467dbfc27c94ab3", size = 16527531, upload-time = "2026-07-04T17:06:26.69Z" }, - { url = "https://files.pythonhosted.org/packages/83/d0/a39b2fbcde9cb17a1dac678f254b33a6336298af9df338824c685425d5e8/numpy-2.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e824c2acf8862052246be5a44c15da1777940c60d010dd2aab897824d9c430f9", size = 18431940, upload-time = "2026-07-04T17:06:29.521Z" }, - { url = "https://files.pythonhosted.org/packages/04/12/cff070947791c1ed425ff76413189adbdc2fbe215eba7ce7fa454a03c7f8/numpy-2.5.1-cp312-cp312-win32.whl", hash = "sha256:08d60c810432eb83360958dea0999ac4cfb94531ea8efcbf0b7f277c2068aeb2", size = 6066764, upload-time = "2026-07-04T17:06:32.571Z" }, - { url = "https://files.pythonhosted.org/packages/65/66/53f31807a48a750f9d748da273bc3fcedd12b27ff1f3e373bfec55ef2dc0/numpy-2.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:f7d60026c0bdb1380e83bfa7a0419c4577ee4b9a08880afcb6dadeb74c649fa2", size = 12430966, upload-time = "2026-07-04T17:06:34.926Z" }, - { url = "https://files.pythonhosted.org/packages/2b/2a/d1a88066b1c14186f5d3c0d18c94f17b064511982bab0578d49ee9d43c29/numpy-2.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:17a25e09640602e10bc8de0e6fa2b3fd68eedd84ba6d7842dc8f32f9ab87bd0b", size = 10350488, upload-time = "2026-07-04T17:06:37.785Z" }, - { url = "https://files.pythonhosted.org/packages/eb/07/ec2a3f0c91761581d4b7104a740791800025983f9a4dc4e73f91a99aeac4/numpy-2.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0bfebd8695f9863592fe744be833a258120b14a9f39da255e8aa8fade2c0ddd1", size = 16796419, upload-time = "2026-07-04T17:06:40.37Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ab/ddb499fc4f8780354395face5b65c7fd107bcd6e1d667a5f07d046956f6f/numpy-2.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30b44a6b53a7ae63c54c089a8726e5563ed302716c5b7ccc85afade40b0e7ff6", size = 11765832, upload-time = "2026-07-04T17:06:42.768Z" }, - { url = "https://files.pythonhosted.org/packages/88/b3/3c28c558a09fc72100c646dac6d2fce8e834c471b0edca01a29996706117/numpy-2.5.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6165343f81b56ef8f514f396989e529b61d9dc709b99421b07e9f3e698e2287d", size = 5325143, upload-time = "2026-07-04T17:06:45.466Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0e/ce19b985bb15c596f4f05954e76cccc77c845083b3b8f938a6c68e523128/numpy-2.5.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4939237038ada79308dda3204ac6462df056b5672b2e25db1149cf873668b3e1", size = 6659749, upload-time = "2026-07-04T17:06:47.288Z" }, - { url = "https://files.pythonhosted.org/packages/2e/20/1ee6614d64332a1bba6411f38e68cb79eec1b2459e20a623777c5c5492a2/numpy-2.5.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c6759f538fb912fc46de0a6b1758ccf7b57bc7c7ebebc23974fdac3de8db0cd", size = 15164716, upload-time = "2026-07-04T17:06:49.494Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a7/2bcd3fdbb87804755c35b729bf8709d62025c5f4cfd7d5b2415997097515/numpy-2.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726558e8db4a5bf7929a70ae50f63abda4daf0efe810e3bfbab95976f75fc1a", size = 16661440, upload-time = "2026-07-04T17:06:52.061Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d7/a41e3310c886fe457d36e670bbf24fae411aca8a7b6ad92a32afd924077c/numpy-2.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3935f3b419b244a02732676fa5317a9193cc596a4c0646db07e5b421229ac9f7", size = 16526305, upload-time = "2026-07-04T17:06:54.605Z" }, - { url = "https://files.pythonhosted.org/packages/53/75/4333a9a707c1edd3a4e1a0c58eca52c0f31e55089fa80db02b5565b24df7/numpy-2.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc932a65ded7ce9013d120845a2514dcccb1a67bfc8deb8d37633762951904a6", size = 18423008, upload-time = "2026-07-04T17:06:57.54Z" }, - { url = "https://files.pythonhosted.org/packages/ee/90/e314a32b1c11a2ffe818ddad3a57b50b4b6e1b6c487192eb50cdef0415d0/numpy-2.5.1-cp313-cp313-win32.whl", hash = "sha256:4b4ff1608417eb7a59da7b967bbb798cacfe071d2caf526a24281cd562072ed9", size = 6063885, upload-time = "2026-07-04T17:07:00.14Z" }, - { url = "https://files.pythonhosted.org/packages/10/70/800b3fca480af32df9e8ea9f3d4a0c8feb4b32d7f195d174eabbda4829ad/numpy-2.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:6c3fe51bc6a16453d452997053454f309e8e0ed7b42d6b361ce4ac8c32913d74", size = 12425674, upload-time = "2026-07-04T17:07:02.387Z" }, - { url = "https://files.pythonhosted.org/packages/8b/0b/196350c122f50f6ca56846f2d71efd5e0d24b7b2e07355e019b2e2c7a11e/numpy-2.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:f7feb014281029e628ba2d5a007407443b06e418b6fe451d1e2adcbc8eba0107", size = 10350256, upload-time = "2026-07-04T17:07:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/db/f4/731b6085a83faf6ca843394cbd5e217280c214399f7e8b21b9f552af0ae2/numpy-2.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c786fe9a5bbe360022e584c5a34cf6b54265c71bd7ec8ac3d8fec38968071f8", size = 16795063, upload-time = "2026-07-04T17:07:07.374Z" }, - { url = "https://files.pythonhosted.org/packages/bf/64/0e215f2048dd11a55bb989ed41b3585ef57452404e638d703a211a3e4157/numpy-2.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32985c896d897419ef8da6917872d80b78ad0ea26d85b23245c7366ffde76d75", size = 11776652, upload-time = "2026-07-04T17:07:09.907Z" }, - { url = "https://files.pythonhosted.org/packages/b5/59/2b844c7a6e9deff69b404a66221e1542937734f65d5e6e39411876053862/numpy-2.5.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:efd736408cc97c79b9e6917338dfc8f06013b2274f992e96b1d9a81a71e2a2c2", size = 5335944, upload-time = "2026-07-04T17:07:12.227Z" }, - { url = "https://files.pythonhosted.org/packages/86/51/9bf7cb2cabcebc9e017e4ec7e6322b378317a542c08b4cb68479c1efc716/numpy-2.5.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:ab84dc6b074fa881cae55bea94cc4f68e285181ba7f32497bf7dee6b1496165b", size = 6656266, upload-time = "2026-07-04T17:07:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/83/3e/fb7615b211b82a32f44d5180a6d421b61f84d4fadd578b48ba4ac34e189f/numpy-2.5.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caf3e317d33d60c37986b452613f4ab51246d0691350c03d0cb4a898627f4a95", size = 15179720, upload-time = "2026-07-04T17:07:16.272Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/0f992cb24560673496c5d68de61913b57166ce530ffda07c1f280e0cc464/numpy-2.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54ad769f17bc2d833b620851989f62054fb9ab93c969d9e1dc3c8e3d56beea21", size = 16664835, upload-time = "2026-07-04T17:07:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/97d6475ee91afe2587797d09446f9d3e475ad4cb681662d824809327b75a/numpy-2.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c12afb53450fa976d4c681c50a7423729a4c51c0465ed9f32b8a9cabbc472373", size = 16539135, upload-time = "2026-07-04T17:07:22.015Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/4db81e4ba0be7e2776b1de68c82aa862c7f8ec27e1b4927d4ae075e20678/numpy-2.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c11c405efc5ff6816d5983c96cdfa215bab3428961243af3ff59b228490438", size = 18426684, upload-time = "2026-07-04T17:07:24.941Z" }, - { url = "https://files.pythonhosted.org/packages/1f/64/c0ba2d90724d450279a7df8f32057241070250a26a7e2b5337d77347f481/numpy-2.5.1-cp314-cp314-win32.whl", hash = "sha256:f2479a47f8d5932d1718168a681ad6e536a9df484c83cfcf9de365e164537ace", size = 6116103, upload-time = "2026-07-04T17:07:27.622Z" }, - { url = "https://files.pythonhosted.org/packages/c1/1a/837f9ed7405adcd7a40538792eb169eddd8fa5630c16a1ef49dae71a30f4/numpy-2.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:24d0eb82c0541d3415a33425db64ae439dffccd7b4dbcb30e7c35120205c506a", size = 12562177, upload-time = "2026-07-04T17:07:29.887Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/49707938b6dd0a78a9178dd93227dc89e4c11af47f5c798d70366e8d0483/numpy-2.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5a4c988b38d261deeeaad9954e3deb091ad905c94e8bb6708654ef1d97f286b0", size = 10627739, upload-time = "2026-07-04T17:07:32.568Z" }, - { url = "https://files.pythonhosted.org/packages/a6/c7/bb4b882cfe7f299cbc8b66e42e7dd78cf9d14e40f9469fc5e3db7e15b3bd/numpy-2.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a33276be12fa045805f477f22482088b66bb758ffbe89a9d21457de863a32e22", size = 11894709, upload-time = "2026-07-04T17:07:34.941Z" }, - { url = "https://files.pythonhosted.org/packages/40/3f/5af7f4a7f6224aef48017aa82bb6174c7a659d724be0c75017b7e64a55b4/numpy-2.5.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f089d7b00756190aacf1f5d34bdf38c3c430ac82b4f868f8cede73380460fce7", size = 5453810, upload-time = "2026-07-04T17:07:37.495Z" }, - { url = "https://files.pythonhosted.org/packages/20/c9/3474309bc94d634d3f9c3eddf03250ecb8c22cd948ef16fef69a77cc5d7b/numpy-2.5.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:09e9bfd8d2cf479c7d174804fb3811c53a8e9f20a37444008606b57d6b7a826d", size = 6761189, upload-time = "2026-07-04T17:07:39.563Z" }, - { url = "https://files.pythonhosted.org/packages/90/8a/558ae39fdd55d7e7f7fef9a84a6e964ac6b23edbd2a07e52bb084500507d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e68d8dd1e7eba712948f2053a29ec86917bc70ba1358df869d9f06649ef9cf09", size = 15225039, upload-time = "2026-07-04T17:07:41.682Z" }, - { url = "https://files.pythonhosted.org/packages/63/27/ca7392b2d030277bdf0273e7d23255b3ee57d57a7c170a6f4fb3981e1e5d/numpy-2.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99d5095fa265a0c4152e7bb12759e14381ef5496152f1ce58f44bdf55c44beb4", size = 16701306, upload-time = "2026-07-04T17:07:44.611Z" }, - { url = "https://files.pythonhosted.org/packages/02/42/03d53ae7996c44d4374a8262e9dc41671fd56cbb98f7d47ef85cf5da4c6b/numpy-2.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ab87a91b3cc3382b8956095bd8f95e00cf679bb81554339be1a2ba404a1473c1", size = 16589955, upload-time = "2026-07-04T17:07:47.694Z" }, - { url = "https://files.pythonhosted.org/packages/7b/15/6c1784ae469640e65db111e9a34b3d0f14d91e8a38b9ce34810ced370dbb/numpy-2.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:224ca51130ef7da85bea2191625181cb4f337f9cb64b471f10c1a12aa8b60077", size = 18464252, upload-time = "2026-07-04T17:07:50.684Z" }, - { url = "https://files.pythonhosted.org/packages/94/a8/f98e50356cf167df656c526c2dfeec2d7dde182f2a3da4b458a5938e2776/numpy-2.5.1-cp314-cp314t-win32.whl", hash = "sha256:6eab239876581b2b3c5a242281b6007bbdbcd1c7085d7709bb57c5929b11e6bf", size = 6263298, upload-time = "2026-07-04T17:07:53.445Z" }, - { url = "https://files.pythonhosted.org/packages/72/ac/96ae880cdecad0b3275d9359fcec72667b49a4863c9f12942e43679dda02/numpy-2.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:83ce9c80d5b521b0d77ddcbe5447c218d247929b6cc056ca5351342accfff0af", size = 12748623, upload-time = "2026-07-04T17:07:55.384Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, -] - -[[package]] -name = "packaging" -version = "26.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, -] - -[[package]] -name = "pandas" -version = "3.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "python-dateutil" }, - { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" }, - { url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" }, - { url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" }, - { url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" }, - { url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" }, - { url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" }, - { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, - { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, - { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, - { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, - { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, - { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, - { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, - { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, - { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, - { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, - { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, - { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, - { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, - { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, - { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, - { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, - { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, - { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, - { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, - { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, - { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, - { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, - { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, - { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, -] - -[[package]] -name = "peewee" -version = "4.2.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/6d/45b139fba589e185dcee447414c2e0efaebb0a247c06329fdc9bd84ba4aa/peewee-4.2.6.tar.gz", hash = "sha256:f40655c64a62eaa447af228e4b84a5e80d3c812d2de34c7b9c621a3ef0c6555c", size = 779493, upload-time = "2026-07-17T18:11:21.3Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/63/26ec789e68e994d64ed957dd6ee5a08bb78c2f401c40e46fc142669cd8b0/peewee-4.2.6-py3-none-any.whl", hash = "sha256:b54c0f6e09c987465f8268bb2b4c1cba2e1b2788fcc0974c8e2233af1fd5af8f", size = 173774, upload-time = "2026-07-17T18:11:19.787Z" }, -] - -[[package]] -name = "pillow" -version = "12.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, - { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, - { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, - { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, - { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, - { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, - { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, - { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, - { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, - { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, - { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, - { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, - { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, - { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, - { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, - { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, - { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, - { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, - { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, - { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, - { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, - { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, - { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, - { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, - { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, - { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, - { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, - { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, - { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, - { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, - { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, - { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, - { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, - { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, - { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, - { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, - { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, - { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, - { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, - { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, - { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, - { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, - { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, - { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, - { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, - { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, - { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, - { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, - { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, - { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, - { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, - { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, - { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, - { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, - { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, - { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, - { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, - { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "protobuf" -version = "7.35.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, - { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, - { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, - { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, -] - -[[package]] -name = "proxy-tools" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/cf/77d3e19b7fabd03895caca7857ef51e4c409e0ca6b37ee6e9f7daa50b642/proxy_tools-0.1.0.tar.gz", hash = "sha256:ccb3751f529c047e2d8a58440d86b205303cf0fe8146f784d1cbcd94f0a28010", size = 2978, upload-time = "2014-05-05T21:02:24.606Z" } - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, -] - -[[package]] -name = "pyluach" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/11/42568c1568a75f8803c59f26d29af01a0890352b7a8e03d41ecda8bfb5dd/pyluach-2.3.0.tar.gz", hash = "sha256:ec6e30669d1df50c9ca160486da44a8195bb4c7a5d3d533990d0c5b03accd281", size = 26910, upload-time = "2025-09-09T20:24:39.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/c8/f96208ade3ca4c23b372497d0788bcf0f2e0ff4310e5ee693366bc33fdf0/pyluach-2.3.0-py3-none-any.whl", hash = "sha256:4497b731aef59508b079dbf5f00bc5bf4329ac45090a6cd37b5a83756f0e69ab", size = 25914, upload-time = "2025-09-09T20:24:37.831Z" }, -] - -[[package]] -name = "pyobjc-core" -version = "12.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b4/b1/729f7458a63758bd21716648a8abcd9a0c8f2d2e9897763c8a1a1c7fd31b/pyobjc_core-12.2.1.tar.gz", hash = "sha256:7a7b9b018402342cf32bf1956366896350fbe5c0478cb3ef59778f77abed7f07", size = 1063383, upload-time = "2026-06-19T16:19:39.357Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/87/16564ef5e4568ee0edd9e712d8111dc8b67621d6bb6ff430646ee2d637dd/pyobjc_core-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:24b76a63caf0b5369d4a377c7c0438cd70df81539057af3db839bfaa3579e04a", size = 6484662, upload-time = "2026-06-19T16:04:44.979Z" }, - { url = "https://files.pythonhosted.org/packages/8c/88/300ad283bed0c971c52dcac6f70113e138169d4ce6d856ddd03d16081e51/pyobjc_core-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a64232bb27ed101d4adc7d42b0e64a6d3331aac7bee7861c037a6777a163f10b", size = 6433347, upload-time = "2026-06-19T16:04:49.341Z" }, - { url = "https://files.pythonhosted.org/packages/3e/1e/b9b0ddffae66996b8779f1f7958adc9f21c13a0448cd3be8d7fe589b5b0f/pyobjc_core-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:af101222762665a4125157906cb4b23f5d5a63d3851d5e0504f72a1eaaa2cfd2", size = 6436004, upload-time = "2026-06-19T16:04:53.257Z" }, - { url = "https://files.pythonhosted.org/packages/8f/26/bd309ede07784c6e5fac4b440c90a5f72a66da7859ed303a9392fe8a5f3f/pyobjc_core-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:efe465e3ecc6fc73f7c7622620345d134a8d34564ab1c29d8247e45f4ed55071", size = 6687044, upload-time = "2026-06-19T16:04:57.42Z" }, - { url = "https://files.pythonhosted.org/packages/bd/8a/cfa4f56939d554dbb342ec6e5226a441e2f552bc2002a0ddf7705bb11bef/pyobjc_core-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2b8fc0531c27277325e113ac00b8a72a82e6145f0a88175b9425d8de814ff69a", size = 6429289, upload-time = "2026-06-19T16:05:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/42/74/446c89bc18103aaa4a00d1fb85ff8acace9a0dc3f362d9678ebf7571e275/pyobjc_core-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9bef500f979e22d54f9da3aaebf6a48f873234b324858bd69256055a318955c7", size = 6690181, upload-time = "2026-06-19T16:05:06.201Z" }, - { url = "https://files.pythonhosted.org/packages/99/c7/0121ee4c616af07ad2de8cd1a286f6978dc9a227eb58b7c2e875cb68a1df/pyobjc_core-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:047c226eeb58a2993ace5e8904e71cc9426ee20d064c617f8fbf32717d37093e", size = 6487078, upload-time = "2026-06-19T16:05:10.093Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a8/cb9fcc150f97d0bf22a2028f88b24cc35949beb1bcc7b8bc5c17d4401677/pyobjc_core-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1188613805336270279570467e4455b74cb6c0f60913ac74c917ee1c37cfaecb", size = 6733064, upload-time = "2026-06-19T16:05:14.313Z" }, -] - -[[package]] -name = "pyobjc-framework-cocoa" -version = "12.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/34/fbe38a204643aa4e1b91391cdce07a34da565a69171ebcad08de7438a556/pyobjc_framework_cocoa-12.2.1.tar.gz", hash = "sha256:b94b37fe5730e5ae1fb0052912cd174e6ec329b0bfba4a012ae5db1014b5864b", size = 3125751, upload-time = "2026-06-19T16:20:05.159Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/d6/dc66ea8519a0475efbccf73f82cc28066339bb300a27f5e1bf91ab1d7002/pyobjc_framework_cocoa-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dc6da84f4fc62cc25463bbb85e77a57b8d5ac6caf9a60702daf2edb601332f15", size = 387298, upload-time = "2026-06-19T16:07:37.412Z" }, - { url = "https://files.pythonhosted.org/packages/f7/cf/1b3b32b2f28f66cc053c3438ef4e6df36a1591945bf05e7399da18d74553/pyobjc_framework_cocoa-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:28b9b8bab1c36efb94744786918752d0c1842f5fbb67e7d5ca97b5f736512080", size = 388113, upload-time = "2026-06-19T16:07:38.9Z" }, - { url = "https://files.pythonhosted.org/packages/cc/46/68e8e4d926a2f70fed0437047bc3f9fe08af8fe620d94d80656ebc3cfa9b/pyobjc_framework_cocoa-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3b74a78fa7803e547b32e5e8ec1b49987b52fe318383e793bc6cd49b80efbd9f", size = 388183, upload-time = "2026-06-19T16:07:40.483Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f3/dfc9af4c9eb2e5389c860ad5ef252be9fe456db09f39d537555dc5057aa1/pyobjc_framework_cocoa-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:dc2eaca2f13c7bcd8e41e51a372e47825dea9dd3126108760eed7ba883d2945c", size = 392275, upload-time = "2026-06-19T16:07:42.078Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c8/b90baa8f3592eded79b4be98fb59d2b8dc16b62361e34292bd95806ebd9f/pyobjc_framework_cocoa-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:b386c324d64ae565c1f6b7dfb77be68f640a1c7c23caa6966ab661131f519561", size = 388357, upload-time = "2026-06-19T16:07:43.364Z" }, - { url = "https://files.pythonhosted.org/packages/98/d8/64a94651b9294702d55e748d94de30e25bc59d0784526be7643f4467eccd/pyobjc_framework_cocoa-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a6c584e2af0813cb2f6103b184e632665a26f58c1bd5b08ffd6e95a19c617f7b", size = 392404, upload-time = "2026-06-19T16:07:44.955Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cc/26e8a7bf1f5e8caa38b7f80d486296f9fd3c97e71ad7e5444ef22e802758/pyobjc_framework_cocoa-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:b6023657b8d6cc049a21bd6b4752425f2f53c42f9f0b02d64c7608cc484bf103", size = 388589, upload-time = "2026-06-19T16:07:46.276Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/eedf743a303ea742b8e082afe3613fb4d6618bc1a48cf2568b004ce906f7/pyobjc_framework_cocoa-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:c685ccd8e266a07cf912a2c5a13b1f2eff2a868a1aff163b4801b4687bd425e1", size = 392691, upload-time = "2026-06-19T16:07:47.477Z" }, -] - -[[package]] -name = "pyobjc-framework-quartz" -version = "12.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3b/f6/2a8b84dbf1fe7c04dd96ea73d991678d4e09a909f51971ecc51629bb2ab4/pyobjc_framework_quartz-12.2.1.tar.gz", hash = "sha256:b3b8b6f71e66147f8ff9e6213864cc8527e3a0b1ee90835b93ce221f4802d9b0", size = 3215521, upload-time = "2026-06-19T16:21:30.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/08/527d1ff856e2f2446b5887be01989cc08f9adaf3de7d4eb13d07826c362f/pyobjc_framework_quartz-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60f29408b4f9ed5391a29c6b63e2aa56ddfb8b66b3fb47962930427981e14462", size = 217998, upload-time = "2026-06-19T16:16:02.978Z" }, - { url = "https://files.pythonhosted.org/packages/14/fc/d7c7b3134cdbd1a487f3f77b5be125d87a6c9e7d9411035739d99335cc0c/pyobjc_framework_quartz-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:de9c8cca7e95290c8d540466af11c7cdfe3a5458e6f56c34006d5b45243f9ed9", size = 219000, upload-time = "2026-06-19T16:16:04.29Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4b/861f91a1565d3189ee899e177b915551fb9a7e2ca25414025a8974f04e74/pyobjc_framework_quartz-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:54c9bc7f507192691841ee4eba5bf36990b259df83ac728efed2d7ea1cd021e4", size = 219403, upload-time = "2026-06-19T16:16:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b5/b27010d2f288737f627f74be6d5549f49c841542365c84b9a3011fe39ce7/pyobjc_framework_quartz-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:bfc0d2badd819823d21df8069dcf9544ce360ed747a8895c51bdb25d8d125f45", size = 224458, upload-time = "2026-06-19T16:16:07.252Z" }, - { url = "https://files.pythonhosted.org/packages/8b/5d/85ffd9d433989205d572a50d625c63b29c05e0c5235a725f15ae1023672c/pyobjc_framework_quartz-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ceb56939c337b36d9d81185ade31f77dc52c85cf79bb16e53e9b32f54b6bb3f5", size = 219769, upload-time = "2026-06-19T16:16:08.814Z" }, - { url = "https://files.pythonhosted.org/packages/e2/d6/b917e4b63d72ea84a27121076f3033f23f6497c0e6ce8d304766c899897f/pyobjc_framework_quartz-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8105c98b798f2bf81c05c54bddeeadbf62f0b5dfec13bd6e719dd2cdf7e1cddf", size = 224717, upload-time = "2026-06-19T16:16:10.215Z" }, - { url = "https://files.pythonhosted.org/packages/04/e2/f3c1ed3228f7430ef5ade23db6f1fcbae99290f177ce5653348fd9e05f4d/pyobjc_framework_quartz-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:bbc214f1a216b5d3651bc832d0ac4589f029f3f37cd6cbb370aac12a7c77942c", size = 219825, upload-time = "2026-06-19T16:16:11.433Z" }, - { url = "https://files.pythonhosted.org/packages/66/2a/2c99a5ad2fe0a11600ea123b8e9a08ff138fcb2ad1e13e376f4bd4aa1d96/pyobjc_framework_quartz-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:ca61624a0b0e6286d8a0f97f47eb9011e4e81e9a339db436d48af527e7065bb1", size = 224770, upload-time = "2026-06-19T16:16:13.035Z" }, -] - -[[package]] -name = "pyobjc-framework-security" -version = "12.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/44/b8/4267b802d8dba6de468e7d0765b05cc4e146fa376ed9f55e0b6461016bef/pyobjc_framework_security-12.2.1.tar.gz", hash = "sha256:d7831b1537f4346892e7f2f0e2b09d79bee98919b0767f4061278d0e03028f2d", size = 181065, upload-time = "2026-06-19T16:21:40.151Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/ac/f2ff946edfaf16b4ce5e31afac5e519f83705c0f4842fd25134ecb8f2f4a/pyobjc_framework_security-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ce461296b003b2ba17c8b65f6339f9d2fd5dcfa2b3b52ddc0a696334cc8974c5", size = 41306, upload-time = "2026-06-19T16:17:16.816Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5b/2719bc4062e6c27083191fd20e365ae02d0bf1c22f4d1a88211e3d96b369/pyobjc_framework_security-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:76ff6e44e62d3e15651540493879bf16687d862c4f10f3cadade757811c8b8d0", size = 41300, upload-time = "2026-06-19T16:17:17.702Z" }, - { url = "https://files.pythonhosted.org/packages/15/90/dccd4cd6877ef208957dc1f3675287d8614a4dcd2a3ee0a5e56f5fb5a1ba/pyobjc_framework_security-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:990013baba29d6f985d8950b23701129b2597b3d16f628b785fe97596d8a8de3", size = 41299, upload-time = "2026-06-19T16:17:18.511Z" }, - { url = "https://files.pythonhosted.org/packages/ce/af/f9e8040e0c3ef6a50392a46ad1df482a666aa615180d40730b00282ff81f/pyobjc_framework_security-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:066a3e5e9d368e7a6ba8dd52be2077a634ef12a54fbfcc78b3b8154a8f988a1d", size = 42179, upload-time = "2026-06-19T16:17:19.48Z" }, - { url = "https://files.pythonhosted.org/packages/c9/3c/76e2a8bb8d5fe48f0e8e25c6abec1609f3667cc39935017badfe9e9603f2/pyobjc_framework_security-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:5319ae49b8874363ab51c6ff4d85d4ea0cfa6d836fe0306e901ba9ae560b880d", size = 41370, upload-time = "2026-06-19T16:17:20.501Z" }, - { url = "https://files.pythonhosted.org/packages/14/6e/7120956e9833b2c70757eec1f65f57c191e00662cf74c4545d88315643fa/pyobjc_framework_security-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:21618431e0dbfbd3d4029445e3118af88e5d7e52ddecf9a2d17c759c51628d85", size = 42926, upload-time = "2026-06-19T16:17:21.425Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ff/0bafc557523e5755f74dd5363386a1e9b03f611e2e36df0737a508cd5ab4/pyobjc_framework_security-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:fa192e9df479375e6242adcadb9a44f32907dd7fe1207608710cd3af65fe3c84", size = 41376, upload-time = "2026-06-19T16:17:22.337Z" }, - { url = "https://files.pythonhosted.org/packages/47/33/33d266117e46fef148caa4f986b3d896cb9bfd76bef48bd761cb60c758ee/pyobjc_framework_security-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:07cd044a7996f9a897040c49055fa3bdf565acac4a25b834a72e60602376146d", size = 42944, upload-time = "2026-06-19T16:17:23.371Z" }, -] - -[[package]] -name = "pyobjc-framework-uniformtypeidentifiers" -version = "12.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/a1/108fa1e5a3dd8aff626f98fb97de370323b290404b04ffa2ef9420665ed3/pyobjc_framework_uniformtypeidentifiers-12.2.1.tar.gz", hash = "sha256:1fb89d13aa3c2df8e6d6536f6df3493fe5a6caefd2a5adebf17c5af3b29ed4a2", size = 20679, upload-time = "2026-06-19T16:21:55.739Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/44/18a7b3c3b4f9f6784fddf64ed5a2c148577d0300705a50e8ab81da8fc71d/pyobjc_framework_uniformtypeidentifiers-12.2.1-py2.py3-none-any.whl", hash = "sha256:ea08413ad895a7dfea13670e26548bcf5b00154084cdfb5d8f96603320e77cf3", size = 5042, upload-time = "2026-06-19T16:18:54.085Z" }, -] - -[[package]] -name = "pyobjc-framework-webkit" -version = "12.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/11/d2/b230c594f70ecb970b4cef67bae2648d1bfa5b381e9b7e3710bf24ec8887/pyobjc_framework_webkit-12.2.1.tar.gz", hash = "sha256:a56acae55b50d549b20dff2921ad1099add8fbc377d0de09ddc2ba50957f7def", size = 332374, upload-time = "2026-06-19T16:22:01.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/d3/2ab99d3975dd4624dd943e5a7c8d37e40258d3c9fcf4f26baf09a24e6c9b/pyobjc_framework_webkit-12.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:af5c4ccdf03845adac082823a3b4341b5b2fe62d2d664550afa705b5286a06fc", size = 50264, upload-time = "2026-06-19T16:19:30.607Z" }, - { url = "https://files.pythonhosted.org/packages/84/47/7a2099eb2e062c6230a9440f1795cf34056ca5e16ef25c8aad7c059b8734/pyobjc_framework_webkit-12.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7e04dcc08cdc59380113ea1232af75a0a04c2426418ebe967b4c0045c973f776", size = 50372, upload-time = "2026-06-19T16:19:31.581Z" }, - { url = "https://files.pythonhosted.org/packages/2a/a4/202ec288808011d3f459d000d593e88b1118f2d1d5a4dfaaf5232f2c2ac2/pyobjc_framework_webkit-12.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:23bee8bf7077f91da4e3ae54a00c7f5e4414319e15f98be8584dbd67c4043fae", size = 50387, upload-time = "2026-06-19T16:19:32.522Z" }, - { url = "https://files.pythonhosted.org/packages/95/a4/f796e94b43a66704b6ae17c747c7b97fd4b79348f1cfa9bef7b008aaa718/pyobjc_framework_webkit-12.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:00ffb254f97e9ffdd0a82c1faa61a07f6072ba900fa8aba70c83c21198b52e4e", size = 50853, upload-time = "2026-06-19T16:19:33.43Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f6/d24716fef19ccc3d880e99029458803f0174c05df310d991eb97ea3a0799/pyobjc_framework_webkit-12.2.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:67030258c3cd66e8495ccfccef3d2d58010ff0209284c5115e5afdb0e9fd6de1", size = 50499, upload-time = "2026-06-19T16:19:34.45Z" }, - { url = "https://files.pythonhosted.org/packages/a8/6c/817119a52efcc229a30ceff56a0641005a431806a1f555e0571626ba313a/pyobjc_framework_webkit-12.2.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:5d91527c9950c79269dd0d70f2bb8668c298dd06930637c1c063ce5f274a87e5", size = 50967, upload-time = "2026-06-19T16:19:35.474Z" }, - { url = "https://files.pythonhosted.org/packages/2d/59/5fac0754d53b2a72aed6f424dfc72e5fa245f83cb57c2e00d02e45390fca/pyobjc_framework_webkit-12.2.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:657825081484c9920c50b76b469b9583f116225b0449c9d95c46cbc8c640adc8", size = 50498, upload-time = "2026-06-19T16:19:36.397Z" }, - { url = "https://files.pythonhosted.org/packages/da/0c/e997e33d99d4ad91da2cf70f0e51ac39b03c58ba210548e9e944bbb421be/pyobjc_framework_webkit-12.2.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:f46adcc6227873f2b14d74b2e789c937f227722274ab59b9fa3c04c6ecb46dd5", size = 50958, upload-time = "2026-06-19T16:19:37.424Z" }, -] - -[[package]] -name = "pystray" -version = "0.19.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pillow" }, - { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, - { name = "python-xlib", marker = "sys_platform == 'linux'" }, - { name = "six" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/64/927a4b9024196a4799eba0180e0ca31568426f258a4a5c90f87a97f51d28/pystray-0.19.5-py2.py3-none-any.whl", hash = "sha256:a0c2229d02cf87207297c22d86ffc57c86c227517b038c0d3c59df79295ac617", size = 49068, upload-time = "2023-09-17T13:44:26.872Z" }, -] - -[[package]] -name = "pytest" -version = "9.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "python-xlib" -version = "0.33" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/b8/ff33610932e0ee81ae7f1269c890f697d56ff74b9f5b2ee5d9b7fa2c5355/python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398", size = 182185, upload-time = "2022-12-25T18:52:58.662Z" }, -] - -[[package]] -name = "pythonnet" -version = "3.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "clr-loader" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/57/da1992e44663b71365c6e842c8d7fa453d4ec45fb99a68cfee5b7e944d3c/pythonnet-3.1.0.tar.gz", hash = "sha256:7b34c382905d10a371509ffafd64cae0416305c28817738a9cd138336f4e9991", size = 250599, upload-time = "2026-05-23T20:30:21.578Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/4b/52414f442624d2589f5374a48c08d5ae94f24bea67fc13a20a752884e5b7/pythonnet-3.1.0-cp310.cp311.cp312.cp313.cp314-none-any.whl", hash = "sha256:698dd88edc198819ad63b624a6ebe76208c7b46e4fe13626f65e484f0358d6ba", size = 217578, upload-time = "2026-05-23T20:30:19.527Z" }, - { url = "https://files.pythonhosted.org/packages/db/67/031124fdcb937c266a3265118525bbf6dc13b8c79786d6a7290aecb6e7bb/pythonnet-3.1.0-cp310.cp311.cp312.cp313.cp314-none-win32.win_amd64.whl", hash = "sha256:7bdd4de03df3547a48122a3989265c8b31d5be0d19dadffa009eec7df8085e0b", size = 1644898, upload-time = "2026-05-23T20:30:16.213Z" }, -] - -[[package]] -name = "pytz" -version = "2026.3.post1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" }, -] - -[[package]] -name = "pywebview" -version = "6.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bottle" }, - { name = "proxy-tools" }, - { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-uniformtypeidentifiers", marker = "sys_platform == 'darwin'" }, - { name = "pyobjc-framework-webkit", marker = "sys_platform == 'darwin'" }, - { name = "pythonnet", marker = "sys_platform == 'win32'" }, - { name = "qtpy", marker = "sys_platform == 'openbsd6'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/59/4a/05307135dafba67778669d194bd1a3822a7685ec9ee8a6d7e70856c1a551/pywebview-6.2.1.tar.gz", hash = "sha256:71b7136752e40824655304d938efb62014218d1a90bd8e87e1cbdb1ce9c466af", size = 513126, upload-time = "2026-04-15T09:02:16.595Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/25/9491695c22c4842c5b3903b4dc172e0eecf67a27c0af34a71512c9b76a0a/pywebview-6.2.1-py3-none-any.whl", hash = "sha256:9d07275f53894ab4d5e2e0e996227193e7187dec276d9b624dccbce029216b46", size = 525463, upload-time = "2026-04-15T09:02:10.186Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "qtpy" -version = "2.4.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/01/392eba83c8e47b946b929d7c46e0f04b35e9671f8bb6fc36b6f7945b4de8/qtpy-2.4.3.tar.gz", hash = "sha256:db744f7832e6d3da90568ba6ccbca3ee2b3b4a890c3d6fbbc63142f6e4cdf5bb", size = 66982, upload-time = "2025-02-11T15:09:25.759Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/76/37c0ccd5ab968a6a438f9c623aeecc84c202ab2fabc6a8fd927580c15b5a/QtPy-2.4.3-py3-none-any.whl", hash = "sha256:72095afe13673e017946cc258b8d5da43314197b741ed2890e563cf384b51aa1", size = 95045, upload-time = "2025-02-11T15:09:24.162Z" }, -] - -[[package]] -name = "requests" -version = "2.34.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, -] - -[[package]] -name = "rich" -version = "15.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, -] - -[[package]] -name = "ruff" -version = "0.15.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, - { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, - { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, - { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, - { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, - { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, - { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, - { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, - { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "soupsieve" -version = "2.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d9/38/e12680bbe6b4f8f3d17adcaf38d26850aa756c85cf4a80e79fc12a018fe8/soupsieve-2.9.1.tar.gz", hash = "sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba", size = 122261, upload-time = "2026-07-21T16:57:17.452Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" }, -] - -[[package]] -name = "toolz" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, -] - -[[package]] -name = "tzdata" -version = "2026.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, -] - -[[package]] -name = "websockets" -version = "16.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" }, - { url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" }, - { url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" }, - { url = "https://files.pythonhosted.org/packages/33/d9/162321f63c7eed558e9e1798ed7a1e34a4f6dab51f35419e4ed7a4907979/websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2", size = 186859, upload-time = "2026-07-17T22:48:53.915Z" }, - { url = "https://files.pythonhosted.org/packages/de/09/87df740f7430ce564bd52402e9c9458d4d0459cc7d2ee29e530c8204851b/websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01", size = 188095, upload-time = "2026-07-17T22:48:55.384Z" }, - { url = "https://files.pythonhosted.org/packages/d2/12/3d2703af7cc095f3c81904c92208cc1ae79affbc67376944b50ee9301f73/websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0", size = 191385, upload-time = "2026-07-17T22:48:56.742Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/986aa0234a964a00f5149cfc46e136e96c8faad1c783474550f40d31aef4/websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29", size = 188653, upload-time = "2026-07-17T22:48:58.134Z" }, - { url = "https://files.pythonhosted.org/packages/35/6b/10f9d03e3970a69ba67bd3b46b87a929b586d0300fadbfe14f57c1f85490/websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512", size = 187426, upload-time = "2026-07-17T22:48:59.515Z" }, - { url = "https://files.pythonhosted.org/packages/56/db/bb3aad62bf63d8bb3f0634b2eabffcfb3677a34bd19492110ff6869cf703/websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3", size = 184882, upload-time = "2026-07-17T22:49:00.916Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4c/c09a2ea9bfbeccce52fdc383e5f28af4bc8843338aabac28c81489af6120/websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57", size = 187584, upload-time = "2026-07-17T22:49:02.283Z" }, - { url = "https://files.pythonhosted.org/packages/c7/8b/31bb4eb4d9eaacf1fdd39d115772a8aeaedfc19b5dc262e57ffbc8a9d42c/websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3", size = 186174, upload-time = "2026-07-17T22:49:03.973Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e4/dc02d725610a1ad49e193ef91a548194d71bdc6cdf27da83067dd1f73995/websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648", size = 187986, upload-time = "2026-07-17T22:49:05.553Z" }, - { url = "https://files.pythonhosted.org/packages/e0/73/30ed84c8bfd14c73d4af29d5ed9323c3073b48e0b7b23b67070f4e7fd59b/websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d", size = 185565, upload-time = "2026-07-17T22:49:06.959Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d3/4be8d4959f51e31b4f8fc0ece12b45bd3b6c0d15ea23b9990d9c11fc805f/websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be", size = 186598, upload-time = "2026-07-17T22:49:08.293Z" }, - { url = "https://files.pythonhosted.org/packages/26/fa/abb38597a52d84ed9cfacadc7a0c6f2db282c0ab23cdf72b58a666a21227/websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81", size = 186834, upload-time = "2026-07-17T22:49:09.766Z" }, - { url = "https://files.pythonhosted.org/packages/59/80/1119ad08a228b90c4eb77fbe48df7836731a605f5f881ba701ca826a4a65/websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57", size = 179940, upload-time = "2026-07-17T22:49:11.196Z" }, - { url = "https://files.pythonhosted.org/packages/71/b2/e511c1c6f64a95c2f3fc54bffda0e14eaa7e9442be605c29270f7589b918/websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a", size = 180239, upload-time = "2026-07-17T22:49:12.519Z" }, - { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" }, - { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" }, - { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" }, - { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" }, - { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" }, - { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" }, - { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" }, - { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" }, - { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" }, - { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" }, - { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, - { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, - { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, - { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, - { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, - { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, - { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, - { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, - { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, - { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, - { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, - { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" }, - { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" }, - { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" }, - { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" }, - { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" }, - { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" }, - { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" }, - { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" }, - { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" }, - { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" }, - { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" }, - { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" }, - { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" }, - { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" }, - { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" }, - { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" }, - { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" }, - { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" }, - { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" }, - { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" }, - { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" }, - { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" }, - { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" }, - { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" }, - { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" }, - { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" }, - { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" }, - { url = "https://files.pythonhosted.org/packages/e1/ed/71fea6e141590cafc40b14dc5943b0845606bee87bdb52a21b6a73eb4311/websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869", size = 177185, upload-time = "2026-07-17T22:50:56.665Z" }, - { url = "https://files.pythonhosted.org/packages/01/ec/00e7eeca200facf9266a83e4cbbf1bed0e67fba1d4d45031d3e5b3d81b5c/websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9", size = 177459, upload-time = "2026-07-17T22:50:58.197Z" }, - { url = "https://files.pythonhosted.org/packages/75/fd/5774c4b33f7c0d8f0c51809c8b3a93456c48e3543579262cfa64eb5f522e/websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e", size = 178294, upload-time = "2026-07-17T22:50:59.641Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/48e2c03d2bd79bb45948841c592d24156312dd5f58cdf8f549febe652fb6/websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d", size = 179190, upload-time = "2026-07-17T22:51:01.129Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3f/73e511ecf2496ceac57dd4ed8388efe2bcf0769338a2dbf242c8366ae87e/websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5", size = 180330, upload-time = "2026-07-17T22:51:02.603Z" }, - { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, -] - -[[package]] -name = "yfinance" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beautifulsoup4" }, - { name = "curl-cffi" }, - { name = "multitasking" }, - { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "pandas" }, - { name = "peewee" }, - { name = "platformdirs" }, - { name = "protobuf" }, - { name = "pytz" }, - { name = "requests" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/75/f1/095457c99cd5fb44802e0dafbb17051e0bfdbf264bd4531556e662f57fbf/yfinance-1.5.1.tar.gz", hash = "sha256:89c48a1d45fb870f8e3066c22643c6911118ede9cead747b48925ce8e01a6940", size = 167897, upload-time = "2026-06-28T18:13:59.593Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/53/ba0f45c93c45cafe010c9fe2f509c70d1ad0f96ae7b6ca93369db0c17942/yfinance-1.5.1-py2.py3-none-any.whl", hash = "sha256:a5c9cfc1b9c990f217b643e4fb92444e023cc02b2bacdea9c1fb472509fdfe22", size = 144223, upload-time = "2026-06-28T18:13:58.349Z" }, -] diff --git a/verifier/protected_manifest.py b/verifier/protected_manifest.py index e7769f6..5104732 100644 --- a/verifier/protected_manifest.py +++ b/verifier/protected_manifest.py @@ -108,16 +108,9 @@ "environments/environment_v1.json", "dopey_live/__init__.py", "dopey_live/control.py", - "dopey_live/dumbmoney_authority.py", - "dopey_live/dumbmoney_command_feed.py", - "dopey_live/dumbmoney_contract_feed.py", - "dopey_live/dumbmoney_journal_anchor.py", - "dopey_live/dumbmoney_promotion.py", - "dopey_live/dumbmoney_windows_service.py", "dopey_live/ledger.py", "dopey_live/models.py", "dopey_live/robinhood_gateway.py", - "dopey_live/robinhood_mcp_direct.py", "dopey_live/service.py", "dopey_live/settlement.py", "dopey_live/signals.py",