skcomms 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {skcomms-0.2.0 → skcomms-0.2.2}/PKG-INFO +1 -1
- {skcomms-0.2.0 → skcomms-0.2.2}/pyproject.toml +1 -1
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/server.py +56 -2
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/ack.py +85 -18
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/api.py +469 -77
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/capauth_validator.py +215 -42
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/cli.py +748 -0
- skcomms-0.2.2/src/skcomms/cluster.py +219 -0
- skcomms-0.2.2/src/skcomms/config.py +415 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_group_gate.py +27 -14
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_pipeline.py +52 -18
- skcomms-0.2.2/src/skcomms/core.py +1471 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/crypto.py +11 -1
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/discovery.py +24 -7
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/federation.py +85 -1
- skcomms-0.2.2/src/skcomms/home.py +293 -0
- skcomms-0.2.2/src/skcomms/housekeeping.py +260 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/integration.py +41 -12
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/key_exchange.py +31 -11
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/mailbox.py +287 -41
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/nostr_discovery.py +165 -5
- skcomms-0.2.2/src/skcomms/observability.py +398 -0
- skcomms-0.2.2/src/skcomms/operator_probe.py +246 -0
- skcomms-0.2.2/src/skcomms/outbox.py +1134 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/outbox_migrate.py +154 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pairing.py +4 -0
- skcomms-0.2.2/src/skcomms/pairing_mirror.py +103 -0
- skcomms-0.2.2/src/skcomms/paths.py +436 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/peers.py +35 -0
- skcomms-0.2.2/src/skcomms/public_pairing.py +286 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/queue.py +23 -6
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/registry.py +10 -4
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/router.py +475 -184
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/signaling.py +37 -7
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/signing.py +11 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/skfed_resolve.py +28 -86
- skcomms-0.2.2/src/skcomms/skworld_manifest.py +82 -0
- skcomms-0.2.2/src/skcomms/ssrf.py +398 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/store_forward.py +32 -11
- skcomms-0.2.2/src/skcomms/syncthing_rail.py +920 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/tofu.py +59 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transport.py +32 -1
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/broker_server.py +9 -4
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/file.py +318 -15
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/http_s2s.py +189 -15
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/syncthing.py +104 -1
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/webrtc.py +78 -6
- skcomms-0.2.2/src/skcomms/trustbackup.py +551 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/PKG-INFO +1 -1
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/SOURCES.txt +51 -14
- skcomms-0.2.2/tests/test_access_nonce_durable.py +216 -0
- skcomms-0.2.2/tests/test_ack_bind.py +380 -0
- skcomms-0.2.2/tests/test_api_cors.py +105 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_api_federation_inbox.py +31 -9
- skcomms-0.2.2/tests/test_api_health.py +97 -0
- skcomms-0.2.2/tests/test_api_send_signed_wire.py +247 -0
- skcomms-0.2.2/tests/test_broker_server.py +92 -0
- skcomms-0.2.2/tests/test_capauth_failclosed.py +249 -0
- skcomms-0.2.2/tests/test_capauth_resolution.py +58 -0
- skcomms-0.2.2/tests/test_capauth_revoked_expired.py +180 -0
- skcomms-0.2.2/tests/test_cluster_schema.py +165 -0
- skcomms-0.2.2/tests/test_consent_ban_gate_fail_closed.py +212 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_group_gate.py +111 -14
- skcomms-0.2.2/tests/test_core_inbound_beacon_skip.py +81 -0
- skcomms-0.2.2/tests/test_core_send_signed.py +251 -0
- skcomms-0.2.2/tests/test_core_stop.py +47 -0
- skcomms-0.2.2/tests/test_deadletter_ops.py +481 -0
- skcomms-0.2.2/tests/test_delivery_semantics.py +268 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_home_scaffold.py +62 -0
- skcomms-0.2.2/tests/test_housekeeping.py +718 -0
- skcomms-0.2.2/tests/test_http_s2s_star_recipient.py +39 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_http_s2s_transport.py +149 -9
- skcomms-0.2.2/tests/test_identity_trust_backup.py +639 -0
- skcomms-0.2.2/tests/test_key_exchange_transport_scoping.py +132 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_mailbox.py +24 -17
- skcomms-0.2.2/tests/test_mailbox_encrypt_at_rest.py +516 -0
- skcomms-0.2.2/tests/test_mailbox_recipient_key.py +40 -0
- skcomms-0.2.2/tests/test_no_subapp_dependency.py +114 -0
- skcomms-0.2.2/tests/test_nonce_durable.py +340 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_nostr_discovery.py +188 -0
- skcomms-0.2.2/tests/test_observability.py +492 -0
- skcomms-0.2.2/tests/test_operator_cli.py +193 -0
- skcomms-0.2.2/tests/test_outbox_home_alignment.py +104 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_outbox_migrate.py +126 -11
- skcomms-0.2.2/tests/test_pairing_mirror.py +285 -0
- skcomms-0.2.2/tests/test_paths_scoping.py +574 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqroute_padding.py +2 -3
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqroute_transport.py +2 -3
- skcomms-0.2.2/tests/test_prune_inbox.py +96 -0
- skcomms-0.2.2/tests/test_public_pairing.py +245 -0
- skcomms-0.2.2/tests/test_queue_bounds.py +831 -0
- skcomms-0.2.2/tests/test_receiver_wire_ttl_expiry.py +50 -0
- skcomms-0.2.2/tests/test_router_failure_log_dedup.py +174 -0
- skcomms-0.2.2/tests/test_router_perm_backoff.py +131 -0
- skcomms-0.2.2/tests/test_router_perm_backoff_fixes.py +156 -0
- skcomms-0.2.2/tests/test_router_quarantine.py +122 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_router_rail_ordering.py +28 -23
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_router_route_anon.py +2 -1
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_router_route_signed.py +3 -2
- skcomms-0.2.2/tests/test_router_star_broadcast_routing.py +139 -0
- skcomms-0.2.2/tests/test_router_unsigned_excludes_https_s2s.py +155 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_ssrf.py +20 -9
- skcomms-0.2.2/tests/test_skworld_manifest.py +114 -0
- skcomms-0.2.2/tests/test_ssrf_guard.py +431 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_store_forward.py +92 -0
- skcomms-0.2.2/tests/test_syncthing_rail.py +415 -0
- skcomms-0.2.2/tests/test_webrtc_bridge_lifecycle.py +404 -0
- skcomms-0.2.2/tests/test_webrtc_signaling_log_dedup.py +54 -0
- skcomms-0.2.0/src/skcomms/cluster.py +0 -67
- skcomms-0.2.0/src/skcomms/config.py +0 -230
- skcomms-0.2.0/src/skcomms/core.py +0 -947
- skcomms-0.2.0/src/skcomms/cot.py +0 -299
- skcomms-0.2.0/src/skcomms/cot_agent.py +0 -173
- skcomms-0.2.0/src/skcomms/cot_client.py +0 -142
- skcomms-0.2.0/src/skcomms/cot_pki.py +0 -622
- skcomms-0.2.0/src/skcomms/cot_server.py +0 -603
- skcomms-0.2.0/src/skcomms/cot_service.py +0 -191
- skcomms-0.2.0/src/skcomms/geo.py +0 -477
- skcomms-0.2.0/src/skcomms/home.py +0 -152
- skcomms-0.2.0/src/skcomms/outbox.py +0 -663
- skcomms-0.2.0/tests/test_ack_bind.py +0 -89
- skcomms-0.2.0/tests/test_broker_server.py +0 -36
- skcomms-0.2.0/tests/test_capauth_failclosed.py +0 -98
- skcomms-0.2.0/tests/test_cot_agent_nameagnostic.py +0 -18
- skcomms-0.2.0/tests/test_cot_beacon_outbox.py +0 -204
- skcomms-0.2.0/tests/test_cot_codec.py +0 -146
- skcomms-0.2.0/tests/test_cot_pki.py +0 -150
- skcomms-0.2.0/tests/test_cot_server.py +0 -216
- skcomms-0.2.0/tests/test_geo.py +0 -247
- skcomms-0.2.0/tests/test_mailbox_encrypt_at_rest.py +0 -173
- {skcomms-0.2.0 → skcomms-0.2.2}/LICENSE +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/README.md +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/setup.cfg +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/__init__.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/__init__.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/audit.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/config.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/exec.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/files.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/grants.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/knowledge.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/registry.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/routing.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/access/wiring.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/__init__.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/base.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/discord.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/factory.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/fake.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/matrix.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/models.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/registry.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/slack.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/adapters/telegram.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/anon_queue.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/anon_transport.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/capabilities.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/compression.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_banfeeds.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_captcha.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_greylist.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_groups.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_moderation.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_policy.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_requests.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_runtime.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_tiering.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/consent_tokens.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/crypto_suites.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/did_router.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/envelope.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/__init__.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/codebook.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/codec.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/emergent.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/gloss.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/handshake.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/macros.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/message.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/glossa/session.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/grants.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/heartbeat.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/household_router.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/identity.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/marketplace.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/mcp_server.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/metrics.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/models.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/node_registry.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/padding.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pq_provider.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqdm.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqkem.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqkem_backend.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqroute.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqroute_transport.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pqsig.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/profile_router.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pubsub.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/pubsub_transport.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/ratelimit.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/realm.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/skfed_announce.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/skfed_directory.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/skfed_readdr.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/souls_router.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/__init__.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/audio_track.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/__init__.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/gatt.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/identity.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/node.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/noise.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/protocol.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/radio.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/ble/relay.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/__init__.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/addressing.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/framing.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/interface.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/meshtastic_iface.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/store.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/lora/transport.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/nostr.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/p2p_connector.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/p2p_manager.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/p2p_session.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/signaling_base.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/signaling_broker.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/signaling_mailbox.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/tailscale.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/video_track.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/webrtc_media.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms/transports/websocket.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/dependency_links.txt +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/entry_points.txt +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/requires.txt +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/src/skcomms.egg-info/top_level.txt +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_exec.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_files.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_rbac.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_routing.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_server.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_access_wiring.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_adapter_factory.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_adapter_fake.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_anon_queue.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_anon_transport.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_api_access_token.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_api_consent.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_api_consent_token_gate.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_api_skfed_directory.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_backfill_file_locations.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_fragment.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_gatt.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_identity.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_mesh.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_noise.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_pairing_bundle.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_protocol.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_radio.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_ble_relay.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_capabilities.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_capauth_key_reconcile.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_channel_adapter.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_config.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_banfeeds.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_captcha.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_cli.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_cli_mgmt.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_greylist.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_groups.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_moderation.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_pipeline.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_policy.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_requests.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_runtime.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_tiering.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_consent_tokens.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_core_send_federated.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_crypto_failclosed.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_crypto_suites_q0.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_daemon_adapter_lifecycle.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_discord_adapter.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_encrypt_fail_closed.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_envelope_consent_token.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_envelope_v1.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_federation.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_federation_integration.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_codebook.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_codec_l0.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_codec_l1.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_codec_l2.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_emergent.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_emergent_negotiator.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_emergent_protocol.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_gloss.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_handshake.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_handshake_lexicon.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_macros.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_macros_render.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_message.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_session.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_session_macros.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_glossa_to_human.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_grants.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_hybrid_envelope_sig.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_identity_env_override.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_identity_key_binding.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_identity_realm.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_integration.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_knowledge.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_addressing.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_framing.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_interface.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_meshtastic_iface.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_store.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_lora_transport.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_matrix_adapter.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_node_registry.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_p2p_connector.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_p2p_manager.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_p2p_media.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_p2p_session.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_padding.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pairing.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pairing_cli.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_peer_inbox_traversal.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_peers.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqc_cutover.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqc_shim.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqdm.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqkem.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqroute.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_pqsig.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_registry.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_registry_cli.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_sign_fail_closed.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_signaling_broker.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_signaling_mailbox.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_signaling_select.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_announce.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_cli.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_directory.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_operator_pin.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_readdr.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_skfed_resolve.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_slack_adapter.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_smoke.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_syncthing_recipient_guard.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_telegram_adapter.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_tofu.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_typed_message_content.py +0 -0
- {skcomms-0.2.0 → skcomms-0.2.2}/tests/test_webrtc_ice_logging.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "skcomms"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Sovereign communications for AI agents — multi-channel transport + FQID realm addressing"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "GPL-3.0-or-later"}
|
|
@@ -27,9 +27,11 @@ from __future__ import annotations
|
|
|
27
27
|
|
|
28
28
|
import json
|
|
29
29
|
import logging
|
|
30
|
+
import os
|
|
30
31
|
import socket
|
|
31
32
|
from dataclasses import dataclass
|
|
32
|
-
from
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any, Optional, Union
|
|
33
35
|
|
|
34
36
|
from .. import federation as fed
|
|
35
37
|
from ..discovery import PeerStore
|
|
@@ -42,6 +44,49 @@ from .registry import AccessRegistry, DEFAULT_REGISTRY, RegisteredTool, Scope
|
|
|
42
44
|
logger = logging.getLogger("skcomms.access.server")
|
|
43
45
|
|
|
44
46
|
|
|
47
|
+
def _access_nonce_db_path() -> Path:
|
|
48
|
+
"""Resolve the sk-access durable replay-cache path (NODE-LOCAL, never synced).
|
|
49
|
+
|
|
50
|
+
Precedence: ``SKCOMMS_ACCESS_NONCE_DB`` (explicit file path) wins, then
|
|
51
|
+
the ``SKCOMMS_NONCE_CACHE_DIR`` directory override, then the XDG state
|
|
52
|
+
default (``$XDG_STATE_HOME/skcomms/`` or ``~/.local/state/skcomms/``):
|
|
53
|
+
see :func:`skcomms.home.resolve_nonce_db`. It is a SEPARATE file from
|
|
54
|
+
the federation inbox cache (``nonce_cache.db``) so the two surfaces keep
|
|
55
|
+
independent replay history.
|
|
56
|
+
|
|
57
|
+
The store used to live at ``skcomms_home()/state/access_nonce_cache.db``,
|
|
58
|
+
inside the Syncthing-shared tree; a replay cache must never sync between
|
|
59
|
+
nodes, so it now defaults outside any synced dir. A healthy legacy DB is
|
|
60
|
+
migrated once; otherwise a fresh cache starts (safe: replay exposure is
|
|
61
|
+
bounded by the envelope freshness window).
|
|
62
|
+
"""
|
|
63
|
+
override = (os.environ.get("SKCOMMS_ACCESS_NONCE_DB") or "").strip()
|
|
64
|
+
if override:
|
|
65
|
+
return Path(override).expanduser()
|
|
66
|
+
from ..home import resolve_nonce_db
|
|
67
|
+
|
|
68
|
+
return resolve_nonce_db("access_nonce_cache.db")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _default_nonce_cache() -> Union["fed.NonceCache", "fed.DurableNonceCache"]:
|
|
72
|
+
"""Build the sk-access replay guard: durable by default (coord f465b407).
|
|
73
|
+
|
|
74
|
+
Mirrors :func:`skcomms.api._get_nonce_cache` semantics for the sk-access
|
|
75
|
+
surface: a SQLite-backed :class:`~skcomms.federation.DurableNonceCache`
|
|
76
|
+
so a daemon restart does not reopen a replay window on signed tool calls.
|
|
77
|
+
``SKCOMMS_NONCE_CACHE=memory`` opts back into the legacy in-memory cache
|
|
78
|
+
(explicit, single-process/ephemeral use only).
|
|
79
|
+
|
|
80
|
+
Fail-closed: if the durable store cannot be opened this raises, so the
|
|
81
|
+
server refuses to start rather than silently accepting replays with no
|
|
82
|
+
persistence (systemd Restart=always keeps retrying).
|
|
83
|
+
"""
|
|
84
|
+
mode = (os.environ.get("SKCOMMS_NONCE_CACHE") or "").strip().lower()
|
|
85
|
+
if mode == "memory":
|
|
86
|
+
return fed.NonceCache()
|
|
87
|
+
return fed.DurableNonceCache(_access_nonce_db_path())
|
|
88
|
+
|
|
89
|
+
|
|
45
90
|
# ---------------------------------------------------------------------------
|
|
46
91
|
# Errors
|
|
47
92
|
# ---------------------------------------------------------------------------
|
|
@@ -104,6 +149,9 @@ class AccessServer:
|
|
|
104
149
|
verifier: An :class:`~skcomms.signing.EnvelopeVerifier`. If omitted, one
|
|
105
150
|
is created and seeded TOFU-style from the :class:`PeerStore`.
|
|
106
151
|
peer_store: Peer store for TOFU key resolution (default: a fresh one).
|
|
152
|
+
nonce_cache: Replay guard override (tests / embedding). Default is the
|
|
153
|
+
durable per-node store from :func:`_default_nonce_cache`, so a
|
|
154
|
+
daemon restart does not reopen a replay window (coord f465b407).
|
|
107
155
|
"""
|
|
108
156
|
|
|
109
157
|
def __init__(
|
|
@@ -113,12 +161,18 @@ class AccessServer:
|
|
|
113
161
|
verifier: Optional[EnvelopeVerifier] = None,
|
|
114
162
|
peer_store: Optional[PeerStore] = None,
|
|
115
163
|
audit: Optional[AccessAuditLog] = None,
|
|
164
|
+
nonce_cache: Optional[
|
|
165
|
+
Union[fed.NonceCache, fed.DurableNonceCache]
|
|
166
|
+
] = None,
|
|
116
167
|
) -> None:
|
|
117
168
|
self.config = config or AccessConfig.load()
|
|
118
169
|
self.registry = registry or DEFAULT_REGISTRY
|
|
119
170
|
self._peer_store = peer_store if peer_store is not None else PeerStore()
|
|
120
171
|
self.verifier = verifier or self._build_verifier()
|
|
121
|
-
|
|
172
|
+
# Durable by default; fail-closed if the store cannot be opened.
|
|
173
|
+
self.nonce_cache = (
|
|
174
|
+
nonce_cache if nonce_cache is not None else _default_nonce_cache()
|
|
175
|
+
)
|
|
122
176
|
self.audit = audit or AccessAuditLog(node=self.config.node_name)
|
|
123
177
|
self._register_builtins()
|
|
124
178
|
|
|
@@ -20,7 +20,7 @@ import logging
|
|
|
20
20
|
from datetime import datetime, timezone
|
|
21
21
|
from enum import Enum
|
|
22
22
|
from pathlib import Path
|
|
23
|
-
from typing import Optional
|
|
23
|
+
from typing import Callable, Optional
|
|
24
24
|
|
|
25
25
|
from pydantic import BaseModel, Field
|
|
26
26
|
|
|
@@ -81,16 +81,27 @@ class AckTracker:
|
|
|
81
81
|
Args:
|
|
82
82
|
acks_dir: Directory for ACK tracking files.
|
|
83
83
|
default_timeout: Default seconds to wait for ACK.
|
|
84
|
+
sender_verifier: Optional callable authenticating an ACK envelope
|
|
85
|
+
against its claimed sender (CapAuth/PGP signature). Fails closed:
|
|
86
|
+
a verifier that returns falsy or raises rejects the ACK.
|
|
84
87
|
"""
|
|
85
88
|
|
|
86
89
|
def __init__(
|
|
87
90
|
self,
|
|
88
91
|
acks_dir: Optional[Path] = None,
|
|
89
92
|
default_timeout: int = DEFAULT_ACK_TIMEOUT,
|
|
93
|
+
sender_verifier: Optional[Callable[[MessageEnvelope], bool]] = None,
|
|
90
94
|
):
|
|
91
95
|
self._dir = acks_dir or Path(SKCOMMS_HOME).expanduser() / ACKS_DIR_NAME
|
|
92
96
|
self._dir.mkdir(parents=True, exist_ok=True)
|
|
93
97
|
self._default_timeout = default_timeout
|
|
98
|
+
# Optional cryptographic authenticator for inbound ACKs. Given the ACK
|
|
99
|
+
# envelope, returns True iff the ACK is authenticated as coming from its
|
|
100
|
+
# claimed sender (e.g. its CapAuth/PGP payload signature verifies). When
|
|
101
|
+
# wired in, string identity alone is not trusted. Reuses the existing
|
|
102
|
+
# signing stack (skcomms.crypto.EnvelopeCrypto.verify_signature), no new
|
|
103
|
+
# crypto scheme.
|
|
104
|
+
self._sender_verifier = sender_verifier
|
|
94
105
|
|
|
95
106
|
@property
|
|
96
107
|
def acks_dir(self) -> Path:
|
|
@@ -124,16 +135,51 @@ class AckTracker:
|
|
|
124
135
|
logger.debug("Tracking ACK for %s -> %s", envelope.envelope_id[:8], envelope.recipient)
|
|
125
136
|
return pending
|
|
126
137
|
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _identity_matches(a: str, b: str) -> bool:
|
|
140
|
+
"""Compare two identities (agent name, FQID, or PGP fingerprint).
|
|
141
|
+
|
|
142
|
+
Whitespace- and case-insensitive: a PGP fingerprint may be stored
|
|
143
|
+
spaced or unspaced as it crosses different transports. Empty values
|
|
144
|
+
never match (fail closed).
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
a: First identity string.
|
|
148
|
+
b: Second identity string.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
True if both are non-empty and equal after normalization.
|
|
152
|
+
"""
|
|
153
|
+
if not a or not b:
|
|
154
|
+
return False
|
|
155
|
+
return a.replace(" ", "").casefold() == b.replace(" ", "").casefold()
|
|
156
|
+
|
|
127
157
|
def process_ack(self, ack_envelope: MessageEnvelope) -> Optional[PendingAck]:
|
|
128
158
|
"""Process a received ACK envelope and resolve the pending entry.
|
|
129
159
|
|
|
130
|
-
The ACK's content holds the original envelope_id.
|
|
160
|
+
The ACK's content holds the original envelope_id. Before an ACK is
|
|
161
|
+
allowed to confirm delivery it is bound to the sender identity and
|
|
162
|
+
checked for replay:
|
|
163
|
+
|
|
164
|
+
* Sender binding (anti-forgery): only the party the original message
|
|
165
|
+
was delivered to (``pending.recipient``) may acknowledge it. An ACK
|
|
166
|
+
whose ``sender`` is not that recipient is rejected, so a third party
|
|
167
|
+
cannot forge a delivery confirmation just by learning the
|
|
168
|
+
``envelope_id``.
|
|
169
|
+
* Cryptographic authentication (optional): when a ``sender_verifier``
|
|
170
|
+
is wired in, the ACK's CapAuth/PGP signature must authenticate its
|
|
171
|
+
claimed sender; string identity alone is not trusted. Fails closed.
|
|
172
|
+
* Replay / stale rejection: an ACK may confirm a pending entry exactly
|
|
173
|
+
once. A duplicate ACK for an already-confirmed message, or a stale
|
|
174
|
+
ACK replayed after the entry timed out, does not mutate state (no
|
|
175
|
+
re-confirmation, no resurrection).
|
|
131
176
|
|
|
132
177
|
Args:
|
|
133
178
|
ack_envelope: A received ACK-type envelope.
|
|
134
179
|
|
|
135
180
|
Returns:
|
|
136
|
-
The resolved PendingAck, or None if
|
|
181
|
+
The resolved PendingAck, or None if the ACK is unknown, forged,
|
|
182
|
+
unauthenticated, or a replay.
|
|
137
183
|
"""
|
|
138
184
|
if not ack_envelope.is_ack:
|
|
139
185
|
return None
|
|
@@ -142,7 +188,9 @@ class AckTracker:
|
|
|
142
188
|
path = self._dir / f"{original_id}{ACK_SUFFIX}"
|
|
143
189
|
|
|
144
190
|
if not path.exists():
|
|
145
|
-
|
|
191
|
+
# No pending entry: a forged ACK for a message we never sent, or a
|
|
192
|
+
# replay after the entry was purged. Either way, ignore.
|
|
193
|
+
logger.debug("ACK for unknown envelope %s: ignoring", original_id[:8])
|
|
146
194
|
return None
|
|
147
195
|
|
|
148
196
|
try:
|
|
@@ -151,27 +199,46 @@ class AckTracker:
|
|
|
151
199
|
logger.warning("Failed to read pending ACK %s: %s", original_id[:8], exc)
|
|
152
200
|
return None
|
|
153
201
|
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
|
|
202
|
+
# Sender-identity binding (anti-forgery): the ACK must come from the
|
|
203
|
+
# identity the original message was delivered to. Reject any ACK whose
|
|
204
|
+
# sender is not the intended recipient (forgery by a third party).
|
|
205
|
+
# Comparison is fingerprint-normalization aware and fails closed on
|
|
206
|
+
# empty identities.
|
|
207
|
+
if not self._identity_matches(ack_envelope.sender, pending.recipient):
|
|
158
208
|
logger.warning(
|
|
159
|
-
"
|
|
209
|
+
"Rejecting forged ACK for %s: sender %r is not tracked recipient %r",
|
|
160
210
|
original_id[:8],
|
|
161
|
-
|
|
211
|
+
ack_envelope.sender,
|
|
212
|
+
pending.recipient,
|
|
162
213
|
)
|
|
163
214
|
return None
|
|
164
215
|
|
|
165
|
-
#
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
216
|
+
# Optional cryptographic authentication of the ACK sender. Fail closed:
|
|
217
|
+
# a verifier error or a falsy result rejects the ACK.
|
|
218
|
+
if self._sender_verifier is not None:
|
|
219
|
+
try:
|
|
220
|
+
authenticated = bool(self._sender_verifier(ack_envelope))
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
logger.warning(
|
|
223
|
+
"ACK sender verification errored for %s: %s", original_id[:8], exc
|
|
224
|
+
)
|
|
225
|
+
authenticated = False
|
|
226
|
+
if not authenticated:
|
|
227
|
+
logger.warning(
|
|
228
|
+
"Rejecting unauthenticated ACK for %s from %s (signature check failed)",
|
|
229
|
+
original_id[:8],
|
|
230
|
+
ack_envelope.sender,
|
|
231
|
+
)
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
# Replay/dedupe protection: only a still-pending entry may be confirmed.
|
|
235
|
+
# A replayed ACK (or an ACK for an already-resolved or timed-out
|
|
236
|
+
# envelope) is rejected so it cannot re-confirm or mutate the entry.
|
|
237
|
+
if pending.status != AckStatus.PENDING:
|
|
170
238
|
logger.warning(
|
|
171
|
-
"ACK for %s
|
|
239
|
+
"Duplicate/replayed ACK for %s (status=%s): ignoring",
|
|
172
240
|
original_id[:8],
|
|
173
|
-
|
|
174
|
-
pending.recipient,
|
|
241
|
+
pending.status.value,
|
|
175
242
|
)
|
|
176
243
|
return None
|
|
177
244
|
|