skcomms 0.2.2__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.2 → skcomms-0.2.3}/PKG-INFO +1 -1
- {skcomms-0.2.2 → skcomms-0.2.3}/pyproject.toml +1 -1
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/__init__.py +1 -1
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/api.py +66 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/capabilities.py +1 -0
- 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.2 → skcomms-0.2.3}/src/skcomms/operator_probe.py +26 -5
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqdm.py +229 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/PKG-INFO +1 -1
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/SOURCES.txt +8 -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.2 → skcomms-0.2.3}/tests/test_capabilities.py +4 -0
- skcomms-0.2.3/tests/test_geo_roundtrip_skcot.py +141 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_operator_cli.py +20 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_p2p_media.py +10 -3
- skcomms-0.2.3/tests/test_pqdm2.py +108 -0
- skcomms-0.2.3/tests/test_pqdm2_interop.py +80 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/LICENSE +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/README.md +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/setup.cfg +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/__init__.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/audit.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/config.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/exec.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/files.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/grants.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/knowledge.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/registry.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/routing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/server.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/access/wiring.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/ack.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/__init__.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/base.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/discord.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/factory.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/fake.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/matrix.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/models.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/registry.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/slack.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/adapters/telegram.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/anon_queue.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/anon_transport.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/capauth_validator.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/cli.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/cluster.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/compression.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/config.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_banfeeds.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_captcha.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_greylist.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_group_gate.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_groups.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_moderation.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_pipeline.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_policy.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_requests.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_runtime.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_tiering.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/consent_tokens.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/core.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/crypto.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/crypto_suites.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/did_router.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/discovery.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/envelope.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/federation.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/__init__.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/codebook.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/codec.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/emergent.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/gloss.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/handshake.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/macros.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/message.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/glossa/session.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/grants.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/heartbeat.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/home.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/household_router.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/housekeeping.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/identity.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/integration.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/key_exchange.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/mailbox.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/marketplace.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/mcp_server.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/metrics.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/models.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/node_registry.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/nostr_discovery.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/observability.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/outbox.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/outbox_migrate.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/padding.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pairing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pairing_mirror.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/paths.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/peers.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pq_provider.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqkem.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqkem_backend.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqroute.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqroute_transport.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pqsig.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/profile_router.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/public_pairing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pubsub.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/pubsub_transport.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/queue.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/ratelimit.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/realm.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/registry.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/router.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/signaling.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/signing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/skfed_announce.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/skfed_directory.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/skfed_readdr.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/skfed_resolve.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/skworld_manifest.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/souls_router.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/ssrf.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/store_forward.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/syncthing_rail.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/tofu.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transport.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/__init__.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/audio_track.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/__init__.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/gatt.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/identity.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/node.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/noise.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/protocol.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/radio.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/ble/relay.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/broker_server.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/file.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/http_s2s.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/__init__.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/addressing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/framing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/interface.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/meshtastic_iface.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/store.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/lora/transport.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/nostr.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/p2p_connector.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/p2p_manager.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/p2p_session.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/signaling_base.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/signaling_broker.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/signaling_mailbox.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/syncthing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/tailscale.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/video_track.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/webrtc.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/webrtc_media.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/transports/websocket.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms/trustbackup.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/dependency_links.txt +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/entry_points.txt +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/requires.txt +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/src/skcomms.egg-info/top_level.txt +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_exec.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_files.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_nonce_durable.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_rbac.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_routing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_server.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_access_wiring.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ack_bind.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_adapter_factory.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_adapter_fake.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_anon_queue.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_anon_transport.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_access_token.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_consent.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_consent_token_gate.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_cors.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_federation_inbox.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_health.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_send_signed_wire.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_api_skfed_directory.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_backfill_file_locations.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_fragment.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_gatt.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_identity.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_mesh.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_noise.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_pairing_bundle.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_protocol.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_radio.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ble_relay.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_broker_server.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_capauth_failclosed.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_capauth_key_reconcile.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_capauth_resolution.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_capauth_revoked_expired.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_channel_adapter.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_cluster_schema.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_config.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_ban_gate_fail_closed.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_banfeeds.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_captcha.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_cli.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_cli_mgmt.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_greylist.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_group_gate.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_groups.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_moderation.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_pipeline.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_policy.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_requests.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_runtime.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_tiering.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_consent_tokens.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_core_inbound_beacon_skip.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_core_send_federated.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_core_send_signed.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_core_stop.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_crypto_failclosed.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_crypto_suites_q0.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_daemon_adapter_lifecycle.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_deadletter_ops.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_delivery_semantics.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_discord_adapter.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_encrypt_fail_closed.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_envelope_consent_token.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_envelope_v1.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_federation.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_federation_integration.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_codebook.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_codec_l0.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_codec_l1.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_codec_l2.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_emergent.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_emergent_negotiator.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_emergent_protocol.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_gloss.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_handshake.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_handshake_lexicon.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_macros.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_macros_render.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_message.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_session.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_session_macros.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_glossa_to_human.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_grants.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_home_scaffold.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_housekeeping.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_http_s2s_star_recipient.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_http_s2s_transport.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_hybrid_envelope_sig.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_identity_env_override.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_identity_key_binding.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_identity_realm.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_identity_trust_backup.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_integration.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_key_exchange_transport_scoping.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_knowledge.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_addressing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_framing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_interface.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_meshtastic_iface.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_store.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_lora_transport.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_mailbox.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_mailbox_encrypt_at_rest.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_mailbox_recipient_key.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_matrix_adapter.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_no_subapp_dependency.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_node_registry.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_nonce_durable.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_nostr_discovery.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_observability.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_outbox_home_alignment.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_outbox_migrate.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_p2p_connector.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_p2p_manager.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_p2p_session.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_padding.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pairing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pairing_cli.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pairing_mirror.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_paths_scoping.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_peer_inbox_traversal.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_peers.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqc_cutover.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqc_shim.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqdm.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqkem.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqroute.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqroute_padding.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqroute_transport.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_pqsig.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_prune_inbox.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_public_pairing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_queue_bounds.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_receiver_wire_ttl_expiry.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_registry.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_registry_cli.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_failure_log_dedup.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_perm_backoff.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_perm_backoff_fixes.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_quarantine.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_rail_ordering.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_route_anon.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_route_signed.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_star_broadcast_routing.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_router_unsigned_excludes_https_s2s.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_sign_fail_closed.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_signaling_broker.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_signaling_mailbox.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_signaling_select.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_announce.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_cli.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_directory.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_operator_pin.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_readdr.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_resolve.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skfed_ssrf.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_skworld_manifest.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_slack_adapter.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_smoke.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_ssrf_guard.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_store_forward.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_syncthing_rail.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_syncthing_recipient_guard.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_telegram_adapter.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_tofu.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_typed_message_content.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_webrtc_bridge_lifecycle.py +0 -0
- {skcomms-0.2.2 → skcomms-0.2.3}/tests/test_webrtc_ice_logging.py +0 -0
- {skcomms-0.2.2 → 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
|
|
@@ -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...")
|
|
@@ -777,6 +791,58 @@ async def get_capabilities():
|
|
|
777
791
|
return build_capabilities(get_skcomms())
|
|
778
792
|
|
|
779
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
|
+
|
|
780
846
|
class AccessTokenRequest(BaseModel):
|
|
781
847
|
"""Request to mint a capauth token for a single sk-access tool call.
|
|
782
848
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Fleet self-seed for the SKMap geo store (``GET /api/v1/geo/units``).
|
|
2
|
+
|
|
3
|
+
Why this exists
|
|
4
|
+
---------------
|
|
5
|
+
The situational picture served by ``/api/v1/geo/units`` is fed by the CoT
|
|
6
|
+
(Cursor-on-Target) plane. On this fleet the live CoT service (``skcot.service``,
|
|
7
|
+
TAK/ATAK streaming on :8087/:8089/:6969) runs as a **separate process** with its
|
|
8
|
+
own in-memory ``GeoStore``; it only fills up when a real ATAK/iTAK device (or a
|
|
9
|
+
federated peer's CoT) actually beacons a position. Until a device connects, that
|
|
10
|
+
store -- and therefore the ``skcomms-api`` process's own ``get_geo_store()`` -- is
|
|
11
|
+
empty, so the Flutter ``skmap`` pane shows nothing.
|
|
12
|
+
|
|
13
|
+
This module upserts the **sovereign fleet nodes** as CoT units so the map is not
|
|
14
|
+
empty and the whole plumbing (``upsert_from_cot`` -> store -> endpoint ->
|
|
15
|
+
``GeoUnit.fromJson``) is demonstrably working end to end. It is fed through the
|
|
16
|
+
*real* :meth:`skcot.geo.GeoStore.upsert_from_cot` path (building genuine
|
|
17
|
+
:class:`skcot.codec.CotEvent` atoms), not a shortcut, so it exercises exactly the
|
|
18
|
+
code a live feed uses. On a deployment without ``skcot`` installed it falls back
|
|
19
|
+
to the store's plain ``upsert`` so the seed still lands.
|
|
20
|
+
|
|
21
|
+
Clearly a seed, not real telemetry
|
|
22
|
+
-----------------------------------
|
|
23
|
+
Every seeded entity is unmistakable as a placeholder:
|
|
24
|
+
|
|
25
|
+
* ``uid`` / ``callsign`` are prefixed ``fleet:`` (e.g. ``fleet:chiap08``),
|
|
26
|
+
* ``source`` is ``"seed:self"``,
|
|
27
|
+
* coordinates are **placeholders** -- these compute nodes have no GPS. They are
|
|
28
|
+
spread out only so the markers don't stack on the map.
|
|
29
|
+
|
|
30
|
+
Replacing the seed with a real CoT feed
|
|
31
|
+
---------------------------------------
|
|
32
|
+
When a real position feed is wired (an ATAK/iTAK device connecting to
|
|
33
|
+
``skcot.service``, or CoT federated in from a peer), those live units flow into
|
|
34
|
+
the same store and supersede-by-``uid``. The seed units keep their ``fleet:``
|
|
35
|
+
uids and simply coexist; set ``SKCOMMS_GEO_SEED_FLEET=0`` to turn the seed off
|
|
36
|
+
entirely once real telemetry is present.
|
|
37
|
+
|
|
38
|
+
Enablement
|
|
39
|
+
----------
|
|
40
|
+
Off by default (the empty-store contract stays intact for anything that doesn't
|
|
41
|
+
opt in). Set ``SKCOMMS_GEO_SEED_FLEET`` to a truthy value (``1``/``true``/``yes``/
|
|
42
|
+
``on``) to have the ``skcomms-api`` lifespan seed the fleet on startup.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import logging
|
|
48
|
+
import os
|
|
49
|
+
from typing import Any, Optional
|
|
50
|
+
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
# Env flag that opts the api-server lifespan into seeding the fleet on startup.
|
|
54
|
+
SEED_ENV = "SKCOMMS_GEO_SEED_FLEET"
|
|
55
|
+
|
|
56
|
+
# Attribution tag stored on each seeded unit's ``GeoUnit.source``.
|
|
57
|
+
SEED_SOURCE = "seed:self"
|
|
58
|
+
|
|
59
|
+
# CoT type for a friendly ground unit (atom -> classified as ``kind='unit'``).
|
|
60
|
+
_FLEET_COT_TYPE = "a-f-G-U-C"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Known sovereign fleet nodes. ``lat``/``lon`` are PLACEHOLDERS (these nodes have
|
|
64
|
+
# no GPS); they are only spread apart so seeded markers are individually visible.
|
|
65
|
+
# ``hint`` is the tailscale/LAN address for human reference (not a GeoUnit field).
|
|
66
|
+
_FLEET_NODES: list[dict[str, Any]] = [
|
|
67
|
+
{"name": "noroc2027", "hint": "100.108.59.57 / .158 dev+primary (this host)",
|
|
68
|
+
"lat": 39.0000, "lon": -77.0000},
|
|
69
|
+
{"name": "dot100", "hint": "192.168.0.100 RTX 5060 Ti / inference",
|
|
70
|
+
"lat": 39.0200, "lon": -77.0200},
|
|
71
|
+
{"name": "dot41", "hint": "100.86.156.5 / .41 tailscale-only heavy-build",
|
|
72
|
+
"lat": 38.9800, "lon": -77.0200},
|
|
73
|
+
{"name": "chiap08", "hint": "100.81.238.58 terminal node",
|
|
74
|
+
"lat": 39.0200, "lon": -76.9800},
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def is_seed_enabled() -> bool:
|
|
79
|
+
"""Whether the fleet self-seed is opted in via :data:`SEED_ENV`."""
|
|
80
|
+
return os.environ.get(SEED_ENV, "").strip().lower() in {"1", "true", "yes", "on"}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _upsert_node(store: Any, node: dict[str, Any]) -> Optional[str]:
|
|
84
|
+
"""Upsert a single fleet node into *store*; return its uid or ``None``.
|
|
85
|
+
|
|
86
|
+
Feeds through the real ``upsert_from_cot`` path (a genuine CoT atom) when the
|
|
87
|
+
``skcot`` plane is available, else falls back to the store's plain ``upsert``
|
|
88
|
+
(used by the in-process fallback store). Fails soft: logs and returns
|
|
89
|
+
``None`` on any error so one bad node can't abort the whole seed.
|
|
90
|
+
"""
|
|
91
|
+
name = node["name"]
|
|
92
|
+
uid = f"fleet:{name}"
|
|
93
|
+
lat, lon = float(node["lat"]), float(node["lon"])
|
|
94
|
+
try:
|
|
95
|
+
try:
|
|
96
|
+
# Real path: build a CoT atom and let the geo plane classify + store it.
|
|
97
|
+
from skcot.codec import CotEvent, CotPoint
|
|
98
|
+
|
|
99
|
+
cot = CotEvent(
|
|
100
|
+
uid=uid,
|
|
101
|
+
type=_FLEET_COT_TYPE,
|
|
102
|
+
how="m-g",
|
|
103
|
+
point=CotPoint(lat=lat, lon=lon),
|
|
104
|
+
callsign=uid,
|
|
105
|
+
)
|
|
106
|
+
unit = store.upsert_from_cot(cot, source=SEED_SOURCE)
|
|
107
|
+
# upsert_from_cot returns None only if it classified the event as
|
|
108
|
+
# non-mappable; an ``a-*`` atom with a real fix never does.
|
|
109
|
+
return getattr(unit, "uid", uid) if unit is not None else None
|
|
110
|
+
except (ImportError, NotImplementedError):
|
|
111
|
+
# Fallback store (no skcot): upsert a plain GeoUnit-shaped dict.
|
|
112
|
+
store.upsert(
|
|
113
|
+
{
|
|
114
|
+
"uid": uid,
|
|
115
|
+
"callsign": uid,
|
|
116
|
+
"cot_type": _FLEET_COT_TYPE,
|
|
117
|
+
"lat": lat,
|
|
118
|
+
"lon": lon,
|
|
119
|
+
"kind": "unit",
|
|
120
|
+
"source": SEED_SOURCE,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
return uid
|
|
124
|
+
except Exception as exc: # noqa: BLE001 — one bad node must not abort the seed
|
|
125
|
+
logger.warning("geo seed: could not upsert %s (%s): %s", uid, node.get("hint"), exc)
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def seed_fleet(store: Any = None) -> list[str]:
|
|
130
|
+
"""Upsert the sovereign fleet nodes as seed CoT units into *store*.
|
|
131
|
+
|
|
132
|
+
Idempotent: entities are keyed by ``uid`` (``fleet:<name>``), so re-running
|
|
133
|
+
updates in place rather than duplicating. Returns the list of upserted uids.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
store: The geo store to seed. Defaults to the process-global store from
|
|
137
|
+
:func:`skcomms.geo_store.get_geo_store` (the one the endpoint reads).
|
|
138
|
+
"""
|
|
139
|
+
if store is None:
|
|
140
|
+
from .geo_store import get_geo_store
|
|
141
|
+
|
|
142
|
+
store = get_geo_store()
|
|
143
|
+
uids = [uid for node in _FLEET_NODES if (uid := _upsert_node(store, node))]
|
|
144
|
+
logger.info("geo seed: upserted %d fleet unit(s): %s", len(uids), uids)
|
|
145
|
+
return uids
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def seed_fleet_if_enabled(store: Any = None) -> list[str]:
|
|
149
|
+
"""Seed the fleet only when :data:`SEED_ENV` is opted in; else a no-op.
|
|
150
|
+
|
|
151
|
+
The startup hook: safe to call unconditionally from the api-server lifespan.
|
|
152
|
+
Returns the upserted uids (empty when the seed is not enabled).
|
|
153
|
+
"""
|
|
154
|
+
if not is_seed_enabled():
|
|
155
|
+
logger.info("geo seed: %s not set — fleet self-seed disabled", SEED_ENV)
|
|
156
|
+
return []
|
|
157
|
+
return seed_fleet(store)
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Geo / situational-awareness store accessor for the SKComms daemon.
|
|
2
|
+
|
|
3
|
+
Serves ``GET /api/v1/geo/units`` (the backend half of the Flutter ``skmap``
|
|
4
|
+
pane). The situational picture, where every unit / marker / waypoint is, is
|
|
5
|
+
produced by the CoT (Cursor-on-Target) bridge, which now lives in the extracted
|
|
6
|
+
``skcot`` package. This module exposes a single **process-global** store that:
|
|
7
|
+
|
|
8
|
+
* the CoT bridge / service FEEDS →
|
|
9
|
+
``get_geo_store().upsert_from_cot(cot, source=...)``
|
|
10
|
+
(or ``.upsert(unit)`` for a pre-built / federated entity), and
|
|
11
|
+
* the ``/api/v1/geo/units`` endpoint READS →
|
|
12
|
+
``get_geo_store().units_json(include_stale=...)``.
|
|
13
|
+
|
|
14
|
+
That single accessor is the documented seam for wiring the real position feed:
|
|
15
|
+
whoever runs the CoT service inside (or alongside) the daemon upserts into the
|
|
16
|
+
store returned by :func:`get_geo_store`, and the endpoint publishes it.
|
|
17
|
+
|
|
18
|
+
One shared type
|
|
19
|
+
---------------
|
|
20
|
+
There is a single ``GeoUnit`` / ``GeoStore`` type, and it lives in
|
|
21
|
+
:mod:`skcot.geo` (the authoritative, CoT-fed store). This module does **not**
|
|
22
|
+
reimplement it. :class:`SkcotGeoStore` is a *thin adapter* over that shared
|
|
23
|
+
type: it holds a real :class:`skcot.geo.GeoStore` and delegates everything to
|
|
24
|
+
it, adding only one skcomms-side convenience -- dict-coercion on ``upsert`` so
|
|
25
|
+
the fleet self-seed (and tests) can feed plain GeoUnit-shaped dicts. All
|
|
26
|
+
storage, staleness, GeoJSON serialization and CoT classification come from the
|
|
27
|
+
shared skcot type, so the two processes agree on unit serialization by
|
|
28
|
+
construction (a GeoUnit serialized by skcot deserializes identically here).
|
|
29
|
+
|
|
30
|
+
The local adapter store is only ever the *fallback* the endpoint reads when the
|
|
31
|
+
live skcot HTTP bridge (:func:`fetch_skcot_geo`) has no units; it is seeded with
|
|
32
|
+
sovereign fleet placeholders that must not expire, so its inner store is built
|
|
33
|
+
with an infinite TTL (the real freshness clock lives on the live skcot store
|
|
34
|
+
behind the HTTP bridge).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import logging
|
|
41
|
+
import os
|
|
42
|
+
import threading
|
|
43
|
+
import urllib.parse
|
|
44
|
+
import urllib.request
|
|
45
|
+
from typing import Any, Optional
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger("skcomms.geo_store")
|
|
48
|
+
|
|
49
|
+
# --------------------------------------------------------------------------
|
|
50
|
+
# Live skcot bridge (source of truth = skcot's process, not this one).
|
|
51
|
+
# --------------------------------------------------------------------------
|
|
52
|
+
#
|
|
53
|
+
# The real situational picture lives in the `skcot` service process (fed from
|
|
54
|
+
# TCP/TLS/mesh CoT + federation). That process exposes it read-only over HTTP
|
|
55
|
+
# (`skcot.geo_http`, default 127.0.0.1:8091). When that endpoint is reachable
|
|
56
|
+
# AND has units, it is the source of truth; otherwise we fall back to this
|
|
57
|
+
# process's local store (the opt-in fleet seed), so the map endpoint always
|
|
58
|
+
# returns a valid shape and never 500s.
|
|
59
|
+
|
|
60
|
+
SKCOT_GEO_URL_ENV = "SKCOT_GEO_URL"
|
|
61
|
+
DEFAULT_SKCOT_GEO_URL = "http://127.0.0.1:8091/geo/units"
|
|
62
|
+
SKCOT_GEO_TIMEOUT_ENV = "SKCOT_GEO_TIMEOUT_S"
|
|
63
|
+
DEFAULT_SKCOT_GEO_TIMEOUT_S = 0.75
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def skcot_geo_url() -> Optional[str]:
|
|
67
|
+
"""The skcot geo endpoint URL, or None if the bridge is disabled.
|
|
68
|
+
|
|
69
|
+
Defaults to :data:`DEFAULT_SKCOT_GEO_URL`. Set ``SKCOT_GEO_URL`` to an empty
|
|
70
|
+
string to disable the bridge and serve only the local in-process store.
|
|
71
|
+
"""
|
|
72
|
+
url = os.environ.get(SKCOT_GEO_URL_ENV, DEFAULT_SKCOT_GEO_URL)
|
|
73
|
+
return url or None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _skcot_geo_timeout() -> float:
|
|
77
|
+
try:
|
|
78
|
+
return float(os.environ.get(SKCOT_GEO_TIMEOUT_ENV, DEFAULT_SKCOT_GEO_TIMEOUT_S))
|
|
79
|
+
except (TypeError, ValueError):
|
|
80
|
+
return DEFAULT_SKCOT_GEO_TIMEOUT_S
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def fetch_skcot_geo(*, include_stale: bool = False, fmt: str = "units") -> Optional[Any]:
|
|
84
|
+
"""Fetch the live situational picture from skcot's HTTP endpoint.
|
|
85
|
+
|
|
86
|
+
Blocking (stdlib ``urllib``) with a short timeout; call from an async
|
|
87
|
+
handler via ``asyncio.to_thread`` so the event loop is never blocked.
|
|
88
|
+
|
|
89
|
+
Returns the parsed JSON payload (a ``{"units": [...], "count": N}`` dict for
|
|
90
|
+
``fmt="units"``, a GeoJSON ``FeatureCollection`` for ``fmt="geojson"``), or
|
|
91
|
+
``None`` on ANY failure (bridge disabled, connection refused, timeout,
|
|
92
|
+
non-200, unparseable body). Fail-soft: never raises to the caller.
|
|
93
|
+
"""
|
|
94
|
+
url = skcot_geo_url()
|
|
95
|
+
if not url:
|
|
96
|
+
return None
|
|
97
|
+
params: dict[str, str] = {}
|
|
98
|
+
if include_stale:
|
|
99
|
+
params["include_stale"] = "1"
|
|
100
|
+
if fmt == "geojson":
|
|
101
|
+
params["format"] = "geojson"
|
|
102
|
+
if params:
|
|
103
|
+
sep = "&" if urllib.parse.urlsplit(url).query else "?"
|
|
104
|
+
url = url + sep + urllib.parse.urlencode(params)
|
|
105
|
+
try:
|
|
106
|
+
req = urllib.request.Request(url, method="GET")
|
|
107
|
+
with urllib.request.urlopen(req, timeout=_skcot_geo_timeout()) as resp:
|
|
108
|
+
if getattr(resp, "status", 200) != 200:
|
|
109
|
+
return None
|
|
110
|
+
body = resp.read()
|
|
111
|
+
return json.loads(body.decode("utf-8"))
|
|
112
|
+
except Exception as exc: # noqa: BLE001 - map endpoint must never 500 on this
|
|
113
|
+
logger.debug("skcot geo fetch failed (%s): %s", url, exc)
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def geo_payload_has_units(payload: Any) -> bool:
|
|
118
|
+
"""Whether a skcot geo payload actually carries at least one unit.
|
|
119
|
+
|
|
120
|
+
Handles both response shapes: the flat ``{"units": [...], "count": N}`` and
|
|
121
|
+
the GeoJSON ``{"type": "FeatureCollection", "features": [...]}``.
|
|
122
|
+
"""
|
|
123
|
+
if not isinstance(payload, dict):
|
|
124
|
+
return False
|
|
125
|
+
units = payload.get("units")
|
|
126
|
+
if isinstance(units, list):
|
|
127
|
+
return len(units) > 0
|
|
128
|
+
feats = payload.get("features")
|
|
129
|
+
if isinstance(feats, list):
|
|
130
|
+
return len(feats) > 0
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
# --------------------------------------------------------------------------
|
|
134
|
+
# Thin adapter over the ONE shared skcot GeoStore type.
|
|
135
|
+
# --------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class SkcotGeoStore:
|
|
139
|
+
"""Thin skcomms-side adapter over the shared :class:`skcot.geo.GeoStore`.
|
|
140
|
+
|
|
141
|
+
This is **not** a reimplementation. It composes a real
|
|
142
|
+
:class:`skcot.geo.GeoStore` (the single shared type -- CoT classification,
|
|
143
|
+
staleness/TTL, GeoJSON serialization, federation envelopes all live there)
|
|
144
|
+
and delegates every attribute to it via :meth:`__getattr__`. The only thing
|
|
145
|
+
it adds is dict-coercion on :meth:`upsert`, so the fleet self-seed and the
|
|
146
|
+
API can feed plain GeoUnit-shaped dicts as well as real
|
|
147
|
+
:class:`skcot.geo.GeoUnit` objects.
|
|
148
|
+
|
|
149
|
+
``skcot`` is imported lazily (in ``__init__``) rather than at module import
|
|
150
|
+
time: ``skcot`` depends on ``skcomms``, so a top-level import here would be a
|
|
151
|
+
packaging cycle. Because this module is itself imported lazily (only from the
|
|
152
|
+
endpoint / seed paths, after ``skcomms`` has fully loaded), the lazy import
|
|
153
|
+
resolves cleanly.
|
|
154
|
+
|
|
155
|
+
The inner store is built with an infinite TTL by default: the local store is
|
|
156
|
+
only the endpoint's *fallback* (fleet-seed placeholders that must not
|
|
157
|
+
expire); the authoritative freshness clock lives on the live skcot store
|
|
158
|
+
reached over the HTTP bridge.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(self, inner: Any = None, *, ttl_s: float = float("inf")) -> None:
|
|
162
|
+
if inner is None:
|
|
163
|
+
from skcot.geo import GeoStore # type: ignore # lazy: avoid dep cycle
|
|
164
|
+
|
|
165
|
+
inner = GeoStore(ttl_s=ttl_s)
|
|
166
|
+
self._inner = inner
|
|
167
|
+
|
|
168
|
+
def upsert(self, unit: Any) -> Any:
|
|
169
|
+
"""Upsert a :class:`skcot.geo.GeoUnit` or a GeoUnit-shaped dict.
|
|
170
|
+
|
|
171
|
+
A dict is coerced into the shared :class:`skcot.geo.GeoUnit` (unknown
|
|
172
|
+
keys dropped) so the whole store speaks one type; then it is stored by
|
|
173
|
+
the shared store verbatim.
|
|
174
|
+
"""
|
|
175
|
+
if isinstance(unit, dict):
|
|
176
|
+
from skcot.geo import GeoUnit # type: ignore # lazy: avoid dep cycle
|
|
177
|
+
|
|
178
|
+
fields = set(GeoUnit.model_fields)
|
|
179
|
+
unit = GeoUnit(**{k: v for k, v in unit.items() if k in fields})
|
|
180
|
+
return self._inner.upsert(unit)
|
|
181
|
+
|
|
182
|
+
def __getattr__(self, name: str) -> Any:
|
|
183
|
+
# Delegate everything else (upsert_from_cot, units_json,
|
|
184
|
+
# to_feature_collection, get_all, remove, clear, ...) to the shared
|
|
185
|
+
# skcot store. Only reached for names not defined on the adapter.
|
|
186
|
+
return getattr(self._inner, name)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# --------------------------------------------------------------------------
|
|
190
|
+
# Process-global accessor.
|
|
191
|
+
# --------------------------------------------------------------------------
|
|
192
|
+
|
|
193
|
+
_store_lock = threading.Lock()
|
|
194
|
+
_store: Any = None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _new_store() -> Any:
|
|
198
|
+
"""Construct the process-global local store: the shared skcot GeoStore,
|
|
199
|
+
wrapped in the thin :class:`SkcotGeoStore` adapter."""
|
|
200
|
+
return SkcotGeoStore()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def get_geo_store() -> Any:
|
|
204
|
+
"""Return the process-global geo store singleton (creating it on first use).
|
|
205
|
+
|
|
206
|
+
This is the **feed point**: the CoT service upserts into the returned store
|
|
207
|
+
(``upsert_from_cot`` / ``upsert``) and the endpoint reads from it. One store
|
|
208
|
+
per process keeps the daemon's map view and any co-resident agent tool
|
|
209
|
+
reading the same ground truth.
|
|
210
|
+
"""
|
|
211
|
+
global _store
|
|
212
|
+
if _store is None:
|
|
213
|
+
with _store_lock:
|
|
214
|
+
if _store is None:
|
|
215
|
+
_store = _new_store()
|
|
216
|
+
return _store
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def set_geo_store(store: Any) -> None:
|
|
220
|
+
"""Inject a store (used by the CoT service to share its own GeoStore, and
|
|
221
|
+
by tests to seed a known picture)."""
|
|
222
|
+
global _store
|
|
223
|
+
with _store_lock:
|
|
224
|
+
_store = store
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def reset_geo_store() -> None:
|
|
228
|
+
"""Drop the singleton so the next :func:`get_geo_store` rebuilds it (tests)."""
|
|
229
|
+
global _store
|
|
230
|
+
with _store_lock:
|
|
231
|
+
_store = None
|
|
@@ -119,16 +119,36 @@ def _count_queue(pending_dir) -> int:
|
|
|
119
119
|
return sum(1 for f in p.glob("*.json") if f.is_file())
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
def queue_depth() -> int:
|
|
123
|
+
"""The unified PersistentOutbox pending depth: the ONE backlog metric.
|
|
124
|
+
|
|
125
|
+
This is the single canonical outbox-depth probe (coord eb659f61 / roadmap
|
|
126
|
+
CR-5.3). It counts pending entries under the consolidated skcomms
|
|
127
|
+
:class:`~skcomms.outbox.PersistentOutbox` retry store
|
|
128
|
+
(``skcomms.paths.retry_outbox_dir()/pending``, honoring the
|
|
129
|
+
``SKCOMMS_OUTBOX_DIR`` override). Both this module's ``QueueDrained``
|
|
130
|
+
condition and the skchat ``OutboxBounded`` condition (the skchat operator
|
|
131
|
+
CLI and Atlas's skchat adapter, "one probe, two consumers") read this same
|
|
132
|
+
function, so outbox depth is a single source of truth across the fleet.
|
|
133
|
+
|
|
134
|
+
Fails SAFE (returns 0 = drained) when the store cannot be read, so a probe
|
|
135
|
+
failure never raises a false 'outbox flooded' alarm.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
int: Number of pending entries in the unified retry store.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
return _count_queue(_pending_dir())
|
|
142
|
+
except Exception:
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
122
146
|
def _default_probe() -> dict:
|
|
123
147
|
"""Best-effort skcomms health from real signals. Fails SAFE (healthy) when
|
|
124
148
|
skcomms is unreachable, so an inability to probe never raises a false alarm."""
|
|
125
|
-
try:
|
|
126
|
-
depth = _count_queue(_pending_dir())
|
|
127
|
-
except Exception:
|
|
128
|
-
depth = 0
|
|
129
149
|
return {
|
|
130
150
|
"path_healthy": _probe_path_healthy(),
|
|
131
|
-
"queue_depth":
|
|
151
|
+
"queue_depth": queue_depth(),
|
|
132
152
|
"queue_limit": _QUEUE_LIMIT,
|
|
133
153
|
}
|
|
134
154
|
|
|
@@ -243,4 +263,5 @@ __all__ = [
|
|
|
243
263
|
"explain",
|
|
244
264
|
"observe",
|
|
245
265
|
"act",
|
|
266
|
+
"queue_depth",
|
|
246
267
|
]
|