skcomms 0.2.1__tar.gz → 0.2.3__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.1 → skcomms-0.2.3}/PKG-INFO +1 -1
- {skcomms-0.2.1 → skcomms-0.2.3}/pyproject.toml +1 -1
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/__init__.py +1 -1
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/server.py +56 -5
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/api.py +219 -37
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/capabilities.py +1 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/capauth_validator.py +107 -15
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/cli.py +299 -25
- skcomms-0.2.3/src/skcomms/cluster.py +219 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/core.py +56 -13
- skcomms-0.2.3/src/skcomms/geo_seed.py +157 -0
- skcomms-0.2.3/src/skcomms/geo_store.py +231 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/home.py +100 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/integration.py +41 -12
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/key_exchange.py +19 -10
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/mailbox.py +12 -1
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/nostr_discovery.py +165 -5
- skcomms-0.2.3/src/skcomms/operator_probe.py +267 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/outbox.py +21 -4
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pairing.py +4 -0
- skcomms-0.2.3/src/skcomms/pairing_mirror.py +103 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/paths.py +31 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/peers.py +35 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqdm.py +229 -0
- skcomms-0.2.3/src/skcomms/public_pairing.py +286 -0
- skcomms-0.2.3/src/skcomms/skworld_manifest.py +82 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/store_forward.py +3 -2
- skcomms-0.2.3/src/skcomms/syncthing_rail.py +920 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/file.py +23 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/webrtc.py +17 -1
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/trustbackup.py +5 -3
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/PKG-INFO +1 -1
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/SOURCES.txt +28 -0
- skcomms-0.2.3/tests/test_access_nonce_durable.py +216 -0
- skcomms-0.2.3/tests/test_api_geo_seed.py +119 -0
- skcomms-0.2.3/tests/test_api_geo_skcot_bridge.py +223 -0
- skcomms-0.2.3/tests/test_api_geo_units.py +139 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_capabilities.py +4 -0
- skcomms-0.2.3/tests/test_capauth_resolution.py +58 -0
- skcomms-0.2.3/tests/test_capauth_revoked_expired.py +180 -0
- skcomms-0.2.3/tests/test_cluster_schema.py +165 -0
- skcomms-0.2.3/tests/test_core_stop.py +47 -0
- skcomms-0.2.3/tests/test_geo_roundtrip_skcot.py +141 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_housekeeping.py +166 -0
- skcomms-0.2.3/tests/test_key_exchange_transport_scoping.py +132 -0
- skcomms-0.2.3/tests/test_mailbox_recipient_key.py +40 -0
- skcomms-0.2.3/tests/test_no_subapp_dependency.py +114 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_nonce_durable.py +104 -22
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_nostr_discovery.py +188 -0
- skcomms-0.2.3/tests/test_operator_cli.py +213 -0
- skcomms-0.2.3/tests/test_outbox_home_alignment.py +104 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_p2p_media.py +10 -3
- skcomms-0.2.3/tests/test_pairing_mirror.py +285 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_paths_scoping.py +38 -0
- skcomms-0.2.3/tests/test_pqdm2.py +108 -0
- skcomms-0.2.3/tests/test_pqdm2_interop.py +80 -0
- skcomms-0.2.3/tests/test_public_pairing.py +245 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_queue_bounds.py +41 -0
- skcomms-0.2.3/tests/test_skworld_manifest.py +114 -0
- skcomms-0.2.3/tests/test_syncthing_rail.py +415 -0
- skcomms-0.2.3/tests/test_webrtc_bridge_lifecycle.py +404 -0
- skcomms-0.2.1/src/skcomms/cluster.py +0 -67
- {skcomms-0.2.1 → skcomms-0.2.3}/LICENSE +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/README.md +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/setup.cfg +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/__init__.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/audit.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/config.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/exec.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/files.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/grants.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/knowledge.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/registry.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/routing.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/access/wiring.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/ack.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/__init__.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/base.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/discord.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/factory.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/fake.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/matrix.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/models.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/registry.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/slack.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/adapters/telegram.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/anon_queue.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/anon_transport.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/compression.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/config.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_banfeeds.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_captcha.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_greylist.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_group_gate.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_groups.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_moderation.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_pipeline.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_policy.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_requests.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_runtime.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_tiering.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/consent_tokens.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/crypto.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/crypto_suites.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/did_router.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/discovery.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/envelope.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/federation.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/__init__.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/codebook.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/codec.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/emergent.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/gloss.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/handshake.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/macros.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/message.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/glossa/session.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/grants.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/heartbeat.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/household_router.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/housekeeping.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/identity.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/marketplace.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/mcp_server.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/metrics.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/models.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/node_registry.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/observability.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/outbox_migrate.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/padding.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pq_provider.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqkem.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqkem_backend.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqroute.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqroute_transport.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pqsig.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/profile_router.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pubsub.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/pubsub_transport.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/queue.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/ratelimit.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/realm.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/registry.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/router.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/signaling.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/signing.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/skfed_announce.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/skfed_directory.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/skfed_readdr.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/skfed_resolve.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/souls_router.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/ssrf.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/tofu.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transport.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/__init__.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/audio_track.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/__init__.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/gatt.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/identity.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/node.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/noise.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/protocol.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/radio.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/ble/relay.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/broker_server.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/http_s2s.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/__init__.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/addressing.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/framing.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/interface.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/meshtastic_iface.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/store.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/lora/transport.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/nostr.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/p2p_connector.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/p2p_manager.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/p2p_session.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/signaling_base.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/signaling_broker.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/signaling_mailbox.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/syncthing.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/tailscale.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/video_track.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/webrtc_media.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms/transports/websocket.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/dependency_links.txt +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/entry_points.txt +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/requires.txt +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/src/skcomms.egg-info/top_level.txt +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_exec.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_files.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_rbac.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_routing.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_server.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_access_wiring.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ack_bind.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_adapter_factory.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_adapter_fake.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_anon_queue.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_anon_transport.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_access_token.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_consent.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_consent_token_gate.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_cors.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_federation_inbox.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_health.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_send_signed_wire.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_api_skfed_directory.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_backfill_file_locations.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_fragment.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_gatt.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_identity.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_mesh.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_noise.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_pairing_bundle.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_protocol.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_radio.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ble_relay.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_broker_server.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_capauth_failclosed.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_capauth_key_reconcile.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_channel_adapter.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_config.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_ban_gate_fail_closed.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_banfeeds.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_captcha.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_cli.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_cli_mgmt.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_greylist.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_group_gate.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_groups.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_moderation.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_pipeline.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_policy.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_requests.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_runtime.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_tiering.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_consent_tokens.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_core_inbound_beacon_skip.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_core_send_federated.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_core_send_signed.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_crypto_failclosed.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_crypto_suites_q0.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_daemon_adapter_lifecycle.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_deadletter_ops.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_delivery_semantics.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_discord_adapter.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_encrypt_fail_closed.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_envelope_consent_token.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_envelope_v1.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_federation.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_federation_integration.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_codebook.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_codec_l0.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_codec_l1.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_codec_l2.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_emergent.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_emergent_negotiator.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_emergent_protocol.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_gloss.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_handshake.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_handshake_lexicon.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_macros.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_macros_render.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_message.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_session.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_session_macros.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_glossa_to_human.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_grants.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_home_scaffold.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_http_s2s_star_recipient.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_http_s2s_transport.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_hybrid_envelope_sig.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_identity_env_override.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_identity_key_binding.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_identity_realm.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_identity_trust_backup.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_integration.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_knowledge.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_addressing.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_framing.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_interface.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_meshtastic_iface.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_store.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_lora_transport.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_mailbox.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_mailbox_encrypt_at_rest.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_matrix_adapter.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_node_registry.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_observability.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_outbox_migrate.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_p2p_connector.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_p2p_manager.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_p2p_session.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_padding.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pairing.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pairing_cli.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_peer_inbox_traversal.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_peers.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqc_cutover.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqc_shim.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqdm.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqkem.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqroute.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqroute_padding.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqroute_transport.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_pqsig.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_prune_inbox.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_receiver_wire_ttl_expiry.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_registry.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_registry_cli.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_failure_log_dedup.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_perm_backoff.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_perm_backoff_fixes.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_quarantine.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_rail_ordering.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_route_anon.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_route_signed.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_star_broadcast_routing.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_router_unsigned_excludes_https_s2s.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_sign_fail_closed.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_signaling_broker.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_signaling_mailbox.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_signaling_select.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_announce.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_cli.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_directory.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_operator_pin.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_readdr.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_resolve.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_skfed_ssrf.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_slack_adapter.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_smoke.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_ssrf_guard.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_store_forward.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_syncthing_recipient_guard.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_telegram_adapter.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_tofu.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_typed_message_content.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_webrtc_ice_logging.py +0 -0
- {skcomms-0.2.1 → skcomms-0.2.3}/tests/test_webrtc_signaling_log_dedup.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.3"
|
|
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"}
|
|
@@ -15,7 +15,7 @@ FQID / realm layer (pre-alpha stubs, implementations landing in coord tasks):
|
|
|
15
15
|
from skcomms.cluster import ...
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
__version__ = "0.
|
|
18
|
+
__version__ = "0.2.3"
|
|
19
19
|
|
|
20
20
|
from .core import SKComms, SKComm
|
|
21
21
|
from .crypto import EnvelopeCrypto, KeyStore
|
|
@@ -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,15 +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
|
-
#
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
)
|
|
125
176
|
self.audit = audit or AccessAuditLog(node=self.config.node_name)
|
|
126
177
|
self._register_builtins()
|
|
127
178
|
|
|
@@ -27,9 +27,9 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
|
27
27
|
from pydantic import BaseModel, Field
|
|
28
28
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
29
29
|
|
|
30
|
+
from . import skfed_directory
|
|
30
31
|
from .capauth_validator import CapAuthValidator
|
|
31
32
|
from .core import SKComms
|
|
32
|
-
from . import skfed_directory
|
|
33
33
|
from .discovery import PeerInfo, PeerStore
|
|
34
34
|
from .heartbeat import HeartbeatConfig, HeartbeatPublisher
|
|
35
35
|
from .models import (
|
|
@@ -224,6 +224,20 @@ async def lifespan(app: FastAPI):
|
|
|
224
224
|
logger.exception("Failed to start depth-monitor loop")
|
|
225
225
|
_depth_monitor_task = None
|
|
226
226
|
|
|
227
|
+
# Geo self-seed: opt-in (SKCOMMS_GEO_SEED_FLEET) upsert of the sovereign
|
|
228
|
+
# fleet nodes into the process-global geo store so the skmap pane
|
|
229
|
+
# (GET /api/v1/geo/units) shows units before a live CoT device connects.
|
|
230
|
+
# Fed through the real upsert_from_cot path; clearly labeled fleet:<node>
|
|
231
|
+
# seeds; superseded by real CoT once a feed is wired. See geo_seed.py.
|
|
232
|
+
try:
|
|
233
|
+
from .geo_seed import seed_fleet_if_enabled
|
|
234
|
+
|
|
235
|
+
seeded = seed_fleet_if_enabled()
|
|
236
|
+
if seeded:
|
|
237
|
+
logger.info("Geo store seeded with %d fleet unit(s): %s", len(seeded), seeded)
|
|
238
|
+
except Exception:
|
|
239
|
+
logger.exception("Failed to seed geo store")
|
|
240
|
+
|
|
227
241
|
yield
|
|
228
242
|
|
|
229
243
|
logger.info("Shutting down SKComms API server...")
|
|
@@ -642,6 +656,22 @@ async def health():
|
|
|
642
656
|
return body
|
|
643
657
|
|
|
644
658
|
|
|
659
|
+
@app.get("/.well-known/skworld-module.json", tags=["health"])
|
|
660
|
+
async def skworld_module_manifest(request: Request):
|
|
661
|
+
"""skcomms' SKWorld module manifest (public discovery metadata, no bearer).
|
|
662
|
+
|
|
663
|
+
skcomms is a UI-less backend transport service, so this manifest declares no
|
|
664
|
+
UI module: only the operator facet plus a service marker. The shell reads it
|
|
665
|
+
to discover skcomms and learn its health + operator contract before it has a
|
|
666
|
+
token. URLs are origin-relative to the request, so health resolves against
|
|
667
|
+
wherever the api actually answers. Served unauthenticated, like skchat's and
|
|
668
|
+
skcode's manifests.
|
|
669
|
+
"""
|
|
670
|
+
from .skworld_manifest import skcomms_module_manifest
|
|
671
|
+
|
|
672
|
+
return skcomms_module_manifest(str(request.base_url))
|
|
673
|
+
|
|
674
|
+
|
|
645
675
|
# ---------------------------------------------------------------------------
|
|
646
676
|
# MCP tool relay — used by the consciousness-swipe browser extension.
|
|
647
677
|
#
|
|
@@ -761,6 +791,58 @@ async def get_capabilities():
|
|
|
761
791
|
return build_capabilities(get_skcomms())
|
|
762
792
|
|
|
763
793
|
|
|
794
|
+
@app.get("/api/v1/geo/units", tags=["geo"])
|
|
795
|
+
async def get_geo_units(include_stale: bool = False, format: str = "units"):
|
|
796
|
+
"""Current situational picture: live unit / marker / waypoint positions.
|
|
797
|
+
|
|
798
|
+
The backend half of the Flutter ``skmap`` pane. Source of truth is the live
|
|
799
|
+
CoT store in the separate ``skcot`` service process, exposed read-only over
|
|
800
|
+
HTTP (``skcot.geo_http``, default ``http://127.0.0.1:8091/geo/units``, env
|
|
801
|
+
``SKCOT_GEO_URL``). When that bridge is reachable AND has units, its REAL
|
|
802
|
+
telemetry is returned. Otherwise this falls back to the process-global local
|
|
803
|
+
store (see :mod:`skcomms.geo_store`, the opt-in fleet seed). Both paths are
|
|
804
|
+
fail-soft: an empty or unreachable bridge yields the correct shape, never a
|
|
805
|
+
500.
|
|
806
|
+
|
|
807
|
+
Args:
|
|
808
|
+
include_stale: Include fixes past their CoT stale time / TTL. Default
|
|
809
|
+
``False`` (only fresh positions, what a live map wants).
|
|
810
|
+
format: ``"units"`` (default) → ``{"units": [...], "count": N}`` where
|
|
811
|
+
each item is a flat GeoUnit dict (``uid``, ``callsign``, ``lat``,
|
|
812
|
+
``lon``, ``cot_type``, ``last_seen``, ...); ``"geojson"`` → a
|
|
813
|
+
GeoJSON ``FeatureCollection``. Both are accepted by the app's
|
|
814
|
+
``GeoUnit.fromJson`` (which reads ``features`` then ``units``).
|
|
815
|
+
|
|
816
|
+
Returns:
|
|
817
|
+
A dict the ``skmap`` client renders directly.
|
|
818
|
+
"""
|
|
819
|
+
from .geo_store import fetch_skcot_geo, geo_payload_has_units, get_geo_store
|
|
820
|
+
|
|
821
|
+
fmt = "geojson" if format == "geojson" else "units"
|
|
822
|
+
|
|
823
|
+
# Prefer the live skcot store (real CoT). Blocking fetch off the event loop.
|
|
824
|
+
remote = await asyncio.to_thread(
|
|
825
|
+
fetch_skcot_geo, include_stale=include_stale, fmt=fmt
|
|
826
|
+
)
|
|
827
|
+
if remote is not None and geo_payload_has_units(remote):
|
|
828
|
+
return remote
|
|
829
|
+
|
|
830
|
+
# Fall back to the local in-process store (fleet seed) when skcot is
|
|
831
|
+
# unreachable OR reports zero units. Fail-soft: the local store wraps the
|
|
832
|
+
# shared skcot GeoStore, so if the skcot package is somehow absent this
|
|
833
|
+
# still yields the correct empty shape rather than a 500.
|
|
834
|
+
try:
|
|
835
|
+
store = get_geo_store()
|
|
836
|
+
if fmt == "geojson":
|
|
837
|
+
return store.to_feature_collection(include_stale=include_stale)
|
|
838
|
+
units = store.units_json(include_stale=include_stale)
|
|
839
|
+
return {"units": units, "count": len(units)}
|
|
840
|
+
except Exception: # noqa: BLE001 - the map endpoint must never 500
|
|
841
|
+
if fmt == "geojson":
|
|
842
|
+
return {"type": "FeatureCollection", "features": []}
|
|
843
|
+
return {"units": [], "count": 0}
|
|
844
|
+
|
|
845
|
+
|
|
764
846
|
class AccessTokenRequest(BaseModel):
|
|
765
847
|
"""Request to mint a capauth token for a single sk-access tool call.
|
|
766
848
|
|
|
@@ -986,30 +1068,31 @@ _MAX_INBOX_BYTES = 256 * 1024
|
|
|
986
1068
|
|
|
987
1069
|
|
|
988
1070
|
def _nonce_db_path() -> Path:
|
|
989
|
-
"""Resolve the durable nonce-cache SQLite path.
|
|
990
|
-
|
|
991
|
-
``SKCOMMS_NONCE_DB`` (explicit file path) wins
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
and
|
|
1071
|
+
"""Resolve the durable nonce-cache SQLite path (NODE-LOCAL, never synced).
|
|
1072
|
+
|
|
1073
|
+
Precedence: ``SKCOMMS_NONCE_DB`` (explicit file path) wins, then the
|
|
1074
|
+
``SKCOMMS_NONCE_CACHE_DIR`` directory override, then the XDG state
|
|
1075
|
+
default (``$XDG_STATE_HOME/skcomms/`` or ``~/.local/state/skcomms/``):
|
|
1076
|
+
see :func:`skcomms.home.resolve_nonce_db`.
|
|
1077
|
+
|
|
1078
|
+
The store used to live at ``skcomms_home()/state/nonce_cache.db``; on
|
|
1079
|
+
live fleets that home is inside a Syncthing-shared tree, and when the
|
|
1080
|
+
Syncthing folder is rooted ABOVE the home the home's own ``.stignore``
|
|
1081
|
+
``state/`` line has no effect, so two nodes synced one live WAL SQLite
|
|
1082
|
+
(conflict copies observed on .158/.41). A replay guard is strictly
|
|
1083
|
+
per-node state and now defaults outside any synced tree. A healthy
|
|
1084
|
+
legacy DB is migrated once; otherwise a fresh cache starts, which is
|
|
1085
|
+
safe because the replay window is bounded by the envelope freshness
|
|
1086
|
+
check (see SOP.md).
|
|
1002
1087
|
"""
|
|
1003
1088
|
import os as _os
|
|
1004
1089
|
|
|
1005
1090
|
override = (_os.environ.get("SKCOMMS_NONCE_DB") or "").strip()
|
|
1006
1091
|
if override:
|
|
1007
1092
|
return Path(override).expanduser()
|
|
1008
|
-
from .home import
|
|
1093
|
+
from .home import resolve_nonce_db
|
|
1009
1094
|
|
|
1010
|
-
|
|
1011
|
-
ensure_state_ignored(home)
|
|
1012
|
-
return home / "state" / "nonce_cache.db"
|
|
1095
|
+
return resolve_nonce_db("nonce_cache.db")
|
|
1013
1096
|
|
|
1014
1097
|
|
|
1015
1098
|
def _get_nonce_cache():
|
|
@@ -1351,6 +1434,80 @@ async def post_inbox(request: Request):
|
|
|
1351
1434
|
return {"ok": True, "id": env.id}
|
|
1352
1435
|
|
|
1353
1436
|
|
|
1437
|
+
# ---------------------------------------------------------------------------
|
|
1438
|
+
# Public P2P pairing over Tailscale Funnel (card 2ab5aa6c): PUBLIC, opt-in
|
|
1439
|
+
# ---------------------------------------------------------------------------
|
|
1440
|
+
#
|
|
1441
|
+
# Off-tailnet peers cannot reach the tailnet-only mesh, so this route lets a
|
|
1442
|
+
# peer POST their peer bundle to the node's Funnel-exposed pairing endpoint and
|
|
1443
|
+
# bootstrap a P2P pairing. It is the inbound half of ``skcomms.public_pairing``:
|
|
1444
|
+
# the request is routed straight into the EXISTING key exchange
|
|
1445
|
+
# (``import_peer_bundle``), which does all the crypto (armor validation +
|
|
1446
|
+
# fingerprint derivation). The public Funnel is only the bootstrap channel.
|
|
1447
|
+
#
|
|
1448
|
+
# Disabled by default: with no ``SKCOMMS_FUNNEL_URL`` configured the route 404s,
|
|
1449
|
+
# exactly as if it did not exist. When a ``SKCOMMS_FUNNEL_PAIR_TOKEN`` is set,
|
|
1450
|
+
# an inbound request must present a matching token or it is rejected 401.
|
|
1451
|
+
|
|
1452
|
+
_MAX_PAIR_BYTES = 256 * 1024
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
@app.post("/api/v1/pair/public", tags=["pairing"])
|
|
1456
|
+
async def post_public_pairing(request: Request):
|
|
1457
|
+
"""Accept an off-tailnet peer's public pairing request over the Funnel.
|
|
1458
|
+
|
|
1459
|
+
Reads a ``{"bundle": {...}, "token": "..."}`` body, routes it into the
|
|
1460
|
+
existing key exchange, and returns this node's own bundle for mutual
|
|
1461
|
+
bootstrap. 404 when public pairing is not configured (feature off).
|
|
1462
|
+
|
|
1463
|
+
Raises:
|
|
1464
|
+
HTTPException: 404 (feature off), 401 (bad/missing token), 422
|
|
1465
|
+
(missing/malformed bundle), 413 (too large).
|
|
1466
|
+
"""
|
|
1467
|
+
from .key_exchange import KeyExchangeError
|
|
1468
|
+
from .public_pairing import (
|
|
1469
|
+
PublicPairingError,
|
|
1470
|
+
PublicPairingRequest,
|
|
1471
|
+
configured_token,
|
|
1472
|
+
funnel_enabled,
|
|
1473
|
+
handle_public_pairing_request,
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
# Disabled by default: no Funnel base configured -> the route does not exist.
|
|
1477
|
+
if not funnel_enabled():
|
|
1478
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="not found")
|
|
1479
|
+
|
|
1480
|
+
raw = await request.body()
|
|
1481
|
+
if len(raw) > _MAX_PAIR_BYTES:
|
|
1482
|
+
raise HTTPException(
|
|
1483
|
+
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
|
1484
|
+
detail=f"pairing request exceeds {_MAX_PAIR_BYTES} bytes",
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1487
|
+
import json as _json
|
|
1488
|
+
|
|
1489
|
+
try:
|
|
1490
|
+
body = _json.loads(raw or b"{}")
|
|
1491
|
+
req = PublicPairingRequest(**body)
|
|
1492
|
+
except Exception as exc:
|
|
1493
|
+
raise HTTPException(
|
|
1494
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
1495
|
+
detail=f"unparseable pairing request: {exc}",
|
|
1496
|
+
) from exc
|
|
1497
|
+
|
|
1498
|
+
try:
|
|
1499
|
+
result = handle_public_pairing_request(req, expected_token=configured_token())
|
|
1500
|
+
except PublicPairingError as exc:
|
|
1501
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
|
|
1502
|
+
except KeyExchangeError as exc:
|
|
1503
|
+
raise HTTPException(
|
|
1504
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)
|
|
1505
|
+
) from exc
|
|
1506
|
+
|
|
1507
|
+
logger.info("public pairing accepted from %s", (result.get("paired") or {}).get("fqid"))
|
|
1508
|
+
return {"ok": True, **result}
|
|
1509
|
+
|
|
1510
|
+
|
|
1354
1511
|
# ---------------------------------------------------------------------------
|
|
1355
1512
|
# Operator consent surface (skfed-consent-design gate 5) — LOCAL ONLY
|
|
1356
1513
|
# ---------------------------------------------------------------------------
|
|
@@ -2515,25 +2672,50 @@ async def get_webrtc_peers(room: Optional[str] = None):
|
|
|
2515
2672
|
# Consciousness / Soul Snapshot endpoints
|
|
2516
2673
|
# ---------------------------------------------------------------------------
|
|
2517
2674
|
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2675
|
+
# skcapstone.snapshots is an OPTIONAL higher-layer subapp surface. skcomms is
|
|
2676
|
+
# the L0 core and must import without it, so the import is resolved LAZILY on
|
|
2677
|
+
# first use (see ``_load_snapshots``) rather than at module load. These names
|
|
2678
|
+
# stay ``None`` until then; ``from __future__ import annotations`` (top of file)
|
|
2679
|
+
# keeps the string annotations below valid while they are unbound.
|
|
2680
|
+
SnapshotStore = None # type: ignore[assignment]
|
|
2681
|
+
SnapshotIndex = None # type: ignore[assignment]
|
|
2682
|
+
SoulSnapshot = None # type: ignore[assignment]
|
|
2683
|
+
_OOFState = None # type: ignore[assignment]
|
|
2684
|
+
_PersonalityTraits = None # type: ignore[assignment]
|
|
2685
|
+
_ConversationMessage = None # type: ignore[assignment]
|
|
2686
|
+
|
|
2687
|
+
#: Tri-state: ``None`` = not yet probed, ``True``/``False`` = resolved.
|
|
2688
|
+
_SNAPSHOTS_AVAILABLE: Optional[bool] = None
|
|
2689
|
+
|
|
2690
|
+
|
|
2691
|
+
def _load_snapshots() -> bool:
|
|
2692
|
+
"""Resolve ``skcapstone.snapshots`` lazily on first use, caching the result.
|
|
2693
|
+
|
|
2694
|
+
The consciousness/snapshot endpoints legitimately bridge to skcapstone, but
|
|
2695
|
+
deferring the import to first use means merely importing ``skcomms.api``
|
|
2696
|
+
never pulls skcapstone into ``sys.modules``. Returns whether the snapshot
|
|
2697
|
+
types are available; callers 501 when they are not.
|
|
2698
|
+
"""
|
|
2699
|
+
global _SNAPSHOTS_AVAILABLE
|
|
2700
|
+
global SnapshotStore, SnapshotIndex, SoulSnapshot
|
|
2701
|
+
global _OOFState, _PersonalityTraits, _ConversationMessage
|
|
2702
|
+
if _SNAPSHOTS_AVAILABLE is None:
|
|
2703
|
+
try:
|
|
2704
|
+
# ``global`` above routes each of these bindings to the module name.
|
|
2705
|
+
from skcapstone.snapshots import ConversationMessage as _ConversationMessage
|
|
2706
|
+
from skcapstone.snapshots import OOFState as _OOFState
|
|
2707
|
+
from skcapstone.snapshots import PersonalityTraits as _PersonalityTraits
|
|
2708
|
+
from skcapstone.snapshots import (
|
|
2709
|
+
SnapshotIndex,
|
|
2710
|
+
SnapshotStore,
|
|
2711
|
+
SoulSnapshot,
|
|
2712
|
+
)
|
|
2713
|
+
|
|
2714
|
+
_SNAPSHOTS_AVAILABLE = True
|
|
2715
|
+
except Exception: # ImportError, or a broken partial install
|
|
2716
|
+
_SNAPSHOTS_AVAILABLE = False
|
|
2717
|
+
return _SNAPSHOTS_AVAILABLE
|
|
2533
2718
|
|
|
2534
|
-
_SNAPSHOTS_AVAILABLE = True
|
|
2535
|
-
except ImportError:
|
|
2536
|
-
_SNAPSHOTS_AVAILABLE = False
|
|
2537
2719
|
|
|
2538
2720
|
_snapshot_store: Optional[SnapshotStore] = None
|
|
2539
2721
|
|
|
@@ -2548,7 +2730,7 @@ def _get_store() -> "SnapshotStore":
|
|
|
2548
2730
|
HTTPException: If skcapstone is not installed.
|
|
2549
2731
|
"""
|
|
2550
2732
|
global _snapshot_store
|
|
2551
|
-
if not
|
|
2733
|
+
if not _load_snapshots():
|
|
2552
2734
|
raise HTTPException(
|
|
2553
2735
|
status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
2554
2736
|
detail="skcapstone package not installed — cannot manage snapshots",
|
|
@@ -15,8 +15,8 @@ The detached PGP signature covers the UTF-8 string::
|
|
|
15
15
|
A ±300-second window is accepted to tolerate clock skew while preventing
|
|
16
16
|
replay attacks. The signer's public key is resolved from:
|
|
17
17
|
|
|
18
|
-
1. ``~/.skcapstone/skcomms/keys/<FINGERPRINT>.asc``
|
|
19
|
-
2. ``gpg --export --armor <FINGERPRINT>``
|
|
18
|
+
1. ``~/.skcapstone/skcomms/keys/<FINGERPRINT>.asc`` - SKComms per-agent keystore
|
|
19
|
+
2. ``gpg --export --armor <FINGERPRINT>`` - system GPG keyring
|
|
20
20
|
|
|
21
21
|
**Remote validation** is available by setting ``capauth_url`` to a
|
|
22
22
|
CapAuth API base URL. Remote is tried first; on unreachable server the
|
|
@@ -77,7 +77,7 @@ def _find_pubkey_by_fingerprint(
|
|
|
77
77
|
for path in glob.glob(pattern, recursive=True):
|
|
78
78
|
try:
|
|
79
79
|
key, _ = pgpy.PGPKey.from_file(path)
|
|
80
|
-
except Exception: # noqa: BLE001
|
|
80
|
+
except Exception: # noqa: BLE001 - skip unreadable/non-key files
|
|
81
81
|
continue
|
|
82
82
|
if str(key.fingerprint).replace(" ", "").upper() == want:
|
|
83
83
|
return str(key)
|
|
@@ -88,6 +88,78 @@ logger = logging.getLogger("skcomms.capauth_validator")
|
|
|
88
88
|
# PGP fingerprint: 40 hex characters
|
|
89
89
|
_FINGERPRINT_RE = re.compile(r"^[0-9A-Fa-f]{40}$")
|
|
90
90
|
|
|
91
|
+
|
|
92
|
+
def _reject_reason_if_key_unusable(pub_key, signer_keyids) -> Optional[str]:
|
|
93
|
+
"""Return a reason string if the signing key is revoked or expired, else None.
|
|
94
|
+
|
|
95
|
+
SECURITY: pgpy's own ``PGPKey.verify()`` performs NEITHER revocation NOR
|
|
96
|
+
expiry checks (its source literally warns "Revocation checks are not yet
|
|
97
|
+
implemented"), so a signature made by a revoked or expired key verifies as
|
|
98
|
+
valid. That let a revoked/expired CapAuth signing key keep authenticating on
|
|
99
|
+
the WebRTC signaling / SDP auth path (card 6abe9bef).
|
|
100
|
+
|
|
101
|
+
This routes the usability decision through capauth's single source of truth,
|
|
102
|
+
``capauth.crypto.pgpy_backend._assert_key_usable`` (the same helper the
|
|
103
|
+
capauth backend uses, card a93b0528). When that helper cannot be imported we
|
|
104
|
+
fall back to an inline check that mirrors it, so skcomms stays fail-closed
|
|
105
|
+
and never authenticates a revoked/expired signing key even if capauth's
|
|
106
|
+
internal layout changes.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
pub_key: Parsed pgpy public key (primary, with subkeys).
|
|
110
|
+
signer_keyids: Set of 16-hex key IDs that produced the signature being
|
|
111
|
+
verified. Scopes subkey checks to the actual signer.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
A human-readable reason string when the key must be rejected, or None
|
|
115
|
+
when the key is usable.
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
from capauth.crypto.pgpy_backend import _assert_key_usable
|
|
119
|
+
from capauth.exceptions import KeyExpiredError, KeyRevokedError
|
|
120
|
+
except Exception: # noqa: BLE001 - capauth helper unavailable, use fallback
|
|
121
|
+
_assert_key_usable = None
|
|
122
|
+
|
|
123
|
+
if _assert_key_usable is not None:
|
|
124
|
+
try:
|
|
125
|
+
_assert_key_usable(pub_key, signer_keyids)
|
|
126
|
+
return None
|
|
127
|
+
except (KeyRevokedError, KeyExpiredError) as exc:
|
|
128
|
+
return str(exc)
|
|
129
|
+
except Exception as exc: # noqa: BLE001 - unexpected: fail closed
|
|
130
|
+
return f"key usability check errored: {exc}"
|
|
131
|
+
|
|
132
|
+
# Inline fallback mirroring capauth.crypto.pgpy_backend._assert_key_usable.
|
|
133
|
+
try:
|
|
134
|
+
fingerprint = str(pub_key.fingerprint).replace(" ", "")
|
|
135
|
+
for _rev in pub_key.revocation_signatures:
|
|
136
|
+
return f"key {fingerprint} carries a revocation signature"
|
|
137
|
+
if pub_key.is_expired:
|
|
138
|
+
return f"key {fingerprint} is expired"
|
|
139
|
+
for keyid, subkey in pub_key.subkeys.items():
|
|
140
|
+
if signer_keyids and keyid not in signer_keyids:
|
|
141
|
+
continue
|
|
142
|
+
for _rev in subkey.revocation_signatures:
|
|
143
|
+
return f"signing subkey {keyid} of key {fingerprint} carries a revocation signature"
|
|
144
|
+
if signer_keyids and keyid in signer_keyids and subkey.is_expired:
|
|
145
|
+
return f"signing subkey {keyid} of key {fingerprint} is expired"
|
|
146
|
+
except Exception as exc: # noqa: BLE001 - cannot determine usability: fail closed
|
|
147
|
+
return f"could not verify key usability: {exc}"
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _signer_keyids(sig) -> set:
|
|
152
|
+
"""Best-effort set of 16-hex signer key IDs from a pgpy signature object.
|
|
153
|
+
|
|
154
|
+
Returns an empty set when the signer cannot be read; an empty set makes the
|
|
155
|
+
revocation/expiry check apply to the primary key and every subkey (the
|
|
156
|
+
broadest, safest scope).
|
|
157
|
+
"""
|
|
158
|
+
try:
|
|
159
|
+
return {sig.signer}
|
|
160
|
+
except Exception: # noqa: BLE001
|
|
161
|
+
return set()
|
|
162
|
+
|
|
91
163
|
# Replay-prevention window: tokens older than this (or future-dated beyond
|
|
92
164
|
# this) are rejected.
|
|
93
165
|
_TOKEN_WINDOW_SECS = 300 # ±5 minutes
|
|
@@ -152,7 +224,7 @@ class CapAuthValidator:
|
|
|
152
224
|
"""
|
|
153
225
|
if not token:
|
|
154
226
|
if self._require_auth:
|
|
155
|
-
logger.warning("WebRTC signaling: no auth token
|
|
227
|
+
logger.warning("WebRTC signaling: no auth token - rejecting connection")
|
|
156
228
|
return None
|
|
157
229
|
return "anonymous"
|
|
158
230
|
|
|
@@ -233,13 +305,13 @@ class CapAuthValidator:
|
|
|
233
305
|
# Require exactly 3 parts for signed tokens. #
|
|
234
306
|
# ------------------------------------------------------------------ #
|
|
235
307
|
# SECURITY (fail-closed): a token that *looks* signed (>1 part) but is
|
|
236
|
-
# missing its signature CANNOT be verified. Deny it in every mode
|
|
237
|
-
# even permissive
|
|
308
|
+
# missing its signature CANNOT be verified. Deny it in every mode -
|
|
309
|
+
# even permissive - so a peer can never be upgraded to a claimed
|
|
238
310
|
# identity it did not prove. The only permissive convenience is the
|
|
239
311
|
# explicit single-part dev token handled above.
|
|
240
312
|
if len(parts) != 3:
|
|
241
313
|
logger.warning(
|
|
242
|
-
"CapAuth local: expected fingerprint.timestamp.sig, got %d parts
|
|
314
|
+
"CapAuth local: expected fingerprint.timestamp.sig, got %d parts - rejecting",
|
|
243
315
|
len(parts),
|
|
244
316
|
)
|
|
245
317
|
return None
|
|
@@ -279,14 +351,24 @@ class CapAuthValidator:
|
|
|
279
351
|
pub_key = self._load_public_key(fingerprint)
|
|
280
352
|
if pub_key is None:
|
|
281
353
|
# SECURITY (fail-closed): without the signer's public key we
|
|
282
|
-
# cannot verify the signature. Deny in every mode
|
|
354
|
+
# cannot verify the signature. Deny in every mode - never trust
|
|
283
355
|
# the self-asserted fingerprint just because we lack its key.
|
|
284
356
|
logger.warning(
|
|
285
|
-
"CapAuth local: public key not found for %s
|
|
357
|
+
"CapAuth local: public key not found for %s - rejecting (cannot verify)",
|
|
286
358
|
fingerprint,
|
|
287
359
|
)
|
|
288
360
|
return None
|
|
289
361
|
|
|
362
|
+
# SECURITY (fail-closed): reject revoked/expired signing keys before
|
|
363
|
+
# trusting the signature. pgpy.verify() ignores revocation/expiry, so
|
|
364
|
+
# route this decision through capauth's usability check (card 6abe9bef).
|
|
365
|
+
reason = _reject_reason_if_key_unusable(pub_key, _signer_keyids(sig))
|
|
366
|
+
if reason:
|
|
367
|
+
logger.warning(
|
|
368
|
+
"CapAuth local: signing key rejected for %s: %s", fingerprint, reason
|
|
369
|
+
)
|
|
370
|
+
return None
|
|
371
|
+
|
|
290
372
|
# The message that was signed: "capauth:<FINGERPRINT>:<TIMESTAMP>"
|
|
291
373
|
signed_text = f"capauth:{fingerprint}:{timestamp_str}"
|
|
292
374
|
result = pub_key.verify(signed_text, sig)
|
|
@@ -299,7 +381,7 @@ class CapAuthValidator:
|
|
|
299
381
|
|
|
300
382
|
except ImportError:
|
|
301
383
|
logger.warning(
|
|
302
|
-
"pgpy not installed
|
|
384
|
+
"pgpy not installed - cannot verify CapAuth signature; rejecting. "
|
|
303
385
|
"Install skcomms[crypto] for full CapAuth PGP verification."
|
|
304
386
|
)
|
|
305
387
|
# SECURITY (fail-closed): without pgpy we cannot verify the
|
|
@@ -312,7 +394,7 @@ class CapAuthValidator:
|
|
|
312
394
|
# SECURITY (fail-closed): any error mid-verification is a denial,
|
|
313
395
|
# never a fall-through to the claimed identity.
|
|
314
396
|
logger.error(
|
|
315
|
-
"CapAuth local: PGP verification error for %s: %s
|
|
397
|
+
"CapAuth local: PGP verification error for %s: %s - rejecting",
|
|
316
398
|
fingerprint,
|
|
317
399
|
exc,
|
|
318
400
|
)
|
|
@@ -323,8 +405,8 @@ class CapAuthValidator:
|
|
|
323
405
|
|
|
324
406
|
Search order:
|
|
325
407
|
|
|
326
|
-
1. ``~/.skcapstone/skcomms/keys/<FINGERPRINT>.asc``
|
|
327
|
-
2. ``gpg --export --armor <FINGERPRINT>``
|
|
408
|
+
1. ``~/.skcapstone/skcomms/keys/<FINGERPRINT>.asc`` - SKComms per-agent keystore
|
|
409
|
+
2. ``gpg --export --armor <FINGERPRINT>`` - system GPG keyring
|
|
328
410
|
|
|
329
411
|
Args:
|
|
330
412
|
fingerprint: 40-char uppercase hex PGP fingerprint.
|
|
@@ -349,7 +431,7 @@ class CapAuthValidator:
|
|
|
349
431
|
# 2. Per-agent CapAuth identity dirs + skcomms peers store, matched by
|
|
350
432
|
# fingerprint. Reconciles the WebRTC verify path with the mailbox/TOFU
|
|
351
433
|
# layer (2026-06-11) so a *local* agent's signed SDP (e.g. opus↔lumina)
|
|
352
|
-
# verifies
|
|
434
|
+
# verifies - those keys live under capauth/identity, not ~/.skcapstone/skcomms/keys.
|
|
353
435
|
armor = _find_pubkey_by_fingerprint(fingerprint)
|
|
354
436
|
if armor:
|
|
355
437
|
try:
|
|
@@ -434,6 +516,16 @@ class CapAuthValidator:
|
|
|
434
516
|
logger.warning("verify_detached: public key not found for %s", fingerprint)
|
|
435
517
|
return False
|
|
436
518
|
|
|
519
|
+
# SECURITY (fail-closed): reject revoked/expired signing keys before
|
|
520
|
+
# trusting the signature. pgpy.verify() ignores revocation/expiry, so
|
|
521
|
+
# route this decision through capauth's usability check (card 6abe9bef).
|
|
522
|
+
reason = _reject_reason_if_key_unusable(pub_key, _signer_keyids(pgp_sig))
|
|
523
|
+
if reason:
|
|
524
|
+
logger.warning(
|
|
525
|
+
"verify_detached: signing key rejected for %s: %s", fingerprint, reason
|
|
526
|
+
)
|
|
527
|
+
return False
|
|
528
|
+
|
|
437
529
|
result = pub_key.verify(signed_payload, pgp_sig)
|
|
438
530
|
if not bool(result):
|
|
439
531
|
logger.warning("verify_detached: PGP signature INVALID for %s", fingerprint)
|
|
@@ -444,7 +536,7 @@ class CapAuthValidator:
|
|
|
444
536
|
|
|
445
537
|
except ImportError:
|
|
446
538
|
logger.warning(
|
|
447
|
-
"pgpy not installed
|
|
539
|
+
"pgpy not installed - cannot verify SDP signature. "
|
|
448
540
|
"Install skcomms[crypto] for full CapAuth PGP verification."
|
|
449
541
|
)
|
|
450
542
|
return False
|