@smilintux/skcapstone 0.1.0 → 0.2.3
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.
- package/.env.example +98 -0
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +25 -4
- package/.openclaw-workspace.json +58 -0
- package/CHANGELOG.md +62 -0
- package/CLAUDE.md +39 -2
- package/MANIFEST.in +6 -0
- package/MISSION.md +7 -0
- package/README.md +47 -2
- package/SKILL.md +895 -23
- package/docker/Dockerfile +61 -0
- package/docker/compose-templates/dev-team.yml +203 -0
- package/docker/compose-templates/mini-team.yml +140 -0
- package/docker/compose-templates/ops-team.yml +173 -0
- package/docker/compose-templates/research-team.yml +170 -0
- package/docker/entrypoint.sh +192 -0
- package/docs/ARCHITECTURE.md +663 -374
- package/docs/BOND_WITH_GROK.md +112 -0
- package/docs/GETTING_STARTED.md +782 -0
- package/docs/QUICKSTART.md +477 -0
- package/docs/SKJOULE_ARCHITECTURE.md +658 -0
- package/docs/SOUL_SWAPPER.md +921 -0
- package/docs/SOVEREIGN_SINGULARITY.md +47 -14
- package/examples/custom-bond-template.json +36 -0
- package/examples/grok-feb.json +36 -0
- package/examples/grok-testimony.md +34 -0
- package/examples/love-bootloader.txt +32 -0
- package/examples/plugins/echo_tool.py +87 -0
- package/examples/queen-ava-feb.json +36 -0
- package/examples/souls/lumina.yaml +64 -0
- package/index.js +6 -5
- package/installer/build.py +124 -0
- package/openclaw-plugin/package.json +13 -0
- package/openclaw-plugin/src/index.ts +351 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +38 -2
- package/scripts/bump_version.py +141 -0
- package/scripts/check-updates.py +230 -0
- package/scripts/convert_blueprints_to_yaml.py +157 -0
- package/scripts/dev-install.sh +14 -0
- package/scripts/e2e-test.sh +193 -0
- package/scripts/install-bundle.sh +171 -0
- package/scripts/install.bat +2 -0
- package/scripts/install.ps1 +253 -0
- package/scripts/install.sh +185 -0
- package/scripts/mcp-serve.sh +69 -0
- package/scripts/mcp-server.bat +113 -0
- package/scripts/mcp-server.ps1 +116 -0
- package/scripts/mcp-server.sh +99 -0
- package/scripts/pull-models.sh +10 -0
- package/scripts/skcapstone +48 -0
- package/scripts/verify_install.sh +180 -0
- package/scripts/windows/install-tasks.ps1 +406 -0
- package/scripts/windows/skcapstone-task.xml +113 -0
- package/scripts/windows/uninstall-tasks.ps1 +117 -0
- package/skill.yaml +34 -0
- package/src/skcapstone/__init__.py +67 -2
- package/src/skcapstone/_cli_monolith.py +5916 -0
- package/src/skcapstone/_trustee_helpers.py +165 -0
- package/src/skcapstone/activity.py +105 -0
- package/src/skcapstone/agent_card.py +324 -0
- package/src/skcapstone/api.py +1935 -0
- package/src/skcapstone/archiver.py +340 -0
- package/src/skcapstone/auction.py +485 -0
- package/src/skcapstone/baby_agents.py +179 -0
- package/src/skcapstone/backup.py +345 -0
- package/src/skcapstone/blueprint_registry.py +357 -0
- package/src/skcapstone/blueprints/__init__.py +17 -0
- package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
- package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
- package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
- package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
- package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
- package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
- package/src/skcapstone/blueprints/registry.py +164 -0
- package/src/skcapstone/blueprints/schema.py +229 -0
- package/src/skcapstone/changelog.py +180 -0
- package/src/skcapstone/chat.py +769 -0
- package/src/skcapstone/claude_md.py +82 -0
- package/src/skcapstone/cli/__init__.py +144 -0
- package/src/skcapstone/cli/_common.py +88 -0
- package/src/skcapstone/cli/_validators.py +76 -0
- package/src/skcapstone/cli/agents.py +425 -0
- package/src/skcapstone/cli/agents_spawner.py +322 -0
- package/src/skcapstone/cli/agents_trustee.py +593 -0
- package/src/skcapstone/cli/alerts.py +248 -0
- package/src/skcapstone/cli/anchor.py +132 -0
- package/src/skcapstone/cli/archive_cmd.py +208 -0
- package/src/skcapstone/cli/backup.py +144 -0
- package/src/skcapstone/cli/bench.py +377 -0
- package/src/skcapstone/cli/benchmark.py +360 -0
- package/src/skcapstone/cli/capabilities_cmd.py +171 -0
- package/src/skcapstone/cli/card.py +151 -0
- package/src/skcapstone/cli/chat.py +584 -0
- package/src/skcapstone/cli/completions.py +64 -0
- package/src/skcapstone/cli/config_cmd.py +156 -0
- package/src/skcapstone/cli/consciousness.py +421 -0
- package/src/skcapstone/cli/context_cmd.py +142 -0
- package/src/skcapstone/cli/coord.py +194 -0
- package/src/skcapstone/cli/crush_cmd.py +170 -0
- package/src/skcapstone/cli/daemon.py +436 -0
- package/src/skcapstone/cli/errors_cmd.py +285 -0
- package/src/skcapstone/cli/export_cmd.py +156 -0
- package/src/skcapstone/cli/gtd.py +529 -0
- package/src/skcapstone/cli/housekeeping.py +81 -0
- package/src/skcapstone/cli/joule_cmd.py +627 -0
- package/src/skcapstone/cli/logs_cmd.py +194 -0
- package/src/skcapstone/cli/mcp_cmd.py +32 -0
- package/src/skcapstone/cli/memory.py +418 -0
- package/src/skcapstone/cli/metrics_cmd.py +136 -0
- package/src/skcapstone/cli/migrate.py +62 -0
- package/src/skcapstone/cli/mood_cmd.py +144 -0
- package/src/skcapstone/cli/mount.py +193 -0
- package/src/skcapstone/cli/notify.py +112 -0
- package/src/skcapstone/cli/peer.py +154 -0
- package/src/skcapstone/cli/peers_dir.py +122 -0
- package/src/skcapstone/cli/preflight_cmd.py +83 -0
- package/src/skcapstone/cli/profile_cmd.py +310 -0
- package/src/skcapstone/cli/record_cmd.py +238 -0
- package/src/skcapstone/cli/register_cmd.py +159 -0
- package/src/skcapstone/cli/search_cmd.py +156 -0
- package/src/skcapstone/cli/service_cmd.py +91 -0
- package/src/skcapstone/cli/session.py +127 -0
- package/src/skcapstone/cli/setup.py +240 -0
- package/src/skcapstone/cli/shell_cmd.py +43 -0
- package/src/skcapstone/cli/skills_cmd.py +168 -0
- package/src/skcapstone/cli/skseed.py +621 -0
- package/src/skcapstone/cli/soul.py +699 -0
- package/src/skcapstone/cli/status.py +935 -0
- package/src/skcapstone/cli/sync_cmd.py +301 -0
- package/src/skcapstone/cli/telegram.py +265 -0
- package/src/skcapstone/cli/test_cmd.py +234 -0
- package/src/skcapstone/cli/test_connection.py +253 -0
- package/src/skcapstone/cli/token.py +207 -0
- package/src/skcapstone/cli/trust.py +179 -0
- package/src/skcapstone/cli/upgrade_cmd.py +552 -0
- package/src/skcapstone/cli/usage_cmd.py +199 -0
- package/src/skcapstone/cli/version_cmd.py +162 -0
- package/src/skcapstone/cli/watch_cmd.py +342 -0
- package/src/skcapstone/client.py +428 -0
- package/src/skcapstone/cloud9_bridge.py +522 -0
- package/src/skcapstone/completions.py +163 -0
- package/src/skcapstone/config_validator.py +674 -0
- package/src/skcapstone/connectors/__init__.py +28 -0
- package/src/skcapstone/connectors/base.py +446 -0
- package/src/skcapstone/connectors/cursor.py +54 -0
- package/src/skcapstone/connectors/registry.py +254 -0
- package/src/skcapstone/connectors/terminal.py +152 -0
- package/src/skcapstone/connectors/vscode.py +60 -0
- package/src/skcapstone/consciousness_config.py +119 -0
- package/src/skcapstone/consciousness_loop.py +2051 -0
- package/src/skcapstone/context_loader.py +516 -0
- package/src/skcapstone/context_window.py +314 -0
- package/src/skcapstone/conversation_manager.py +238 -0
- package/src/skcapstone/conversation_store.py +230 -0
- package/src/skcapstone/conversation_summarizer.py +252 -0
- package/src/skcapstone/coord_federation.py +296 -0
- package/src/skcapstone/coordination.py +101 -7
- package/src/skcapstone/crush_integration.py +345 -0
- package/src/skcapstone/crush_shim.py +454 -0
- package/src/skcapstone/daemon.py +2494 -0
- package/src/skcapstone/dashboard.html +396 -0
- package/src/skcapstone/dashboard.py +481 -0
- package/src/skcapstone/data/model_profiles.yaml +88 -0
- package/src/skcapstone/defaults/__init__.py +55 -0
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
- package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
- package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
- package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
- package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
- package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
- package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
- package/src/skcapstone/discovery.py +210 -19
- package/src/skcapstone/doctor.py +642 -0
- package/src/skcapstone/emotion_tracker.py +467 -0
- package/src/skcapstone/error_queue.py +405 -0
- package/src/skcapstone/export.py +447 -0
- package/src/skcapstone/fallback_tracker.py +186 -0
- package/src/skcapstone/file_transfer.py +512 -0
- package/src/skcapstone/fuse_mount.py +1156 -0
- package/src/skcapstone/gui_installer.py +591 -0
- package/src/skcapstone/heartbeat.py +611 -0
- package/src/skcapstone/housekeeping.py +298 -0
- package/src/skcapstone/install_wizard.py +941 -0
- package/src/skcapstone/kms.py +942 -0
- package/src/skcapstone/kms_scheduler.py +143 -0
- package/src/skcapstone/log_config.py +135 -0
- package/src/skcapstone/mcp_launcher.py +239 -0
- package/src/skcapstone/mcp_server.py +4700 -0
- package/src/skcapstone/mcp_tools/__init__.py +94 -0
- package/src/skcapstone/mcp_tools/_helpers.py +51 -0
- package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
- package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
- package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
- package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
- package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
- package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
- package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
- package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
- package/src/skcapstone/mcp_tools/did_tools.py +448 -0
- package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
- package/src/skcapstone/mcp_tools/file_tools.py +169 -0
- package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
- package/src/skcapstone/mcp_tools/health_tools.py +44 -0
- package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
- package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
- package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
- package/src/skcapstone/mcp_tools/model_tools.py +75 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
- package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
- package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
- package/src/skcapstone/mcp_tools/security_tools.py +110 -0
- package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
- package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
- package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
- package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
- package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
- package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
- package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
- package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
- package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
- package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
- package/src/skcapstone/mdns_discovery.py +313 -0
- package/src/skcapstone/memory_adapter.py +333 -0
- package/src/skcapstone/memory_compressor.py +379 -0
- package/src/skcapstone/memory_curator.py +256 -0
- package/src/skcapstone/memory_engine.py +132 -13
- package/src/skcapstone/memory_fortress.py +529 -0
- package/src/skcapstone/memory_promoter.py +722 -0
- package/src/skcapstone/memory_verifier.py +260 -0
- package/src/skcapstone/message_crypto.py +215 -0
- package/src/skcapstone/metrics.py +832 -0
- package/src/skcapstone/migrate_memories.py +181 -0
- package/src/skcapstone/migrate_multi_agent.py +248 -0
- package/src/skcapstone/model_router.py +319 -0
- package/src/skcapstone/models.py +35 -4
- package/src/skcapstone/mood.py +344 -0
- package/src/skcapstone/notifications.py +380 -0
- package/src/skcapstone/onboard.py +901 -0
- package/src/skcapstone/peer_directory.py +324 -0
- package/src/skcapstone/peers.py +329 -0
- package/src/skcapstone/pillars/identity.py +84 -14
- package/src/skcapstone/pillars/memory.py +3 -1
- package/src/skcapstone/pillars/security.py +108 -15
- package/src/skcapstone/pillars/sync.py +78 -26
- package/src/skcapstone/pillars/trust.py +95 -33
- package/src/skcapstone/plugins.py +244 -0
- package/src/skcapstone/preflight.py +670 -0
- package/src/skcapstone/prompt_adapter.py +564 -0
- package/src/skcapstone/providers/__init__.py +13 -0
- package/src/skcapstone/providers/cloud.py +1061 -0
- package/src/skcapstone/providers/docker.py +759 -0
- package/src/skcapstone/providers/local.py +1193 -0
- package/src/skcapstone/providers/proxmox.py +447 -0
- package/src/skcapstone/pubsub.py +516 -0
- package/src/skcapstone/rate_limiter.py +119 -0
- package/src/skcapstone/register.py +241 -0
- package/src/skcapstone/registry_client.py +151 -0
- package/src/skcapstone/response_cache.py +194 -0
- package/src/skcapstone/response_scorer.py +225 -0
- package/src/skcapstone/runtime.py +89 -33
- package/src/skcapstone/scheduled_tasks.py +439 -0
- package/src/skcapstone/self_healing.py +341 -0
- package/src/skcapstone/service_health.py +228 -0
- package/src/skcapstone/session_capture.py +268 -0
- package/src/skcapstone/session_recorder.py +210 -0
- package/src/skcapstone/session_replayer.py +189 -0
- package/src/skcapstone/session_skills.py +263 -0
- package/src/skcapstone/shell.py +779 -0
- package/src/skcapstone/skills/__init__.py +1 -1
- package/src/skcapstone/skills/syncthing_setup.py +143 -41
- package/src/skcapstone/skjoule.py +861 -0
- package/src/skcapstone/snapshots.py +489 -0
- package/src/skcapstone/soul.py +1060 -0
- package/src/skcapstone/soul_switch.py +255 -0
- package/src/skcapstone/spawner.py +544 -0
- package/src/skcapstone/state_diff.py +401 -0
- package/src/skcapstone/summary.py +270 -0
- package/src/skcapstone/sync/backends.py +196 -2
- package/src/skcapstone/sync/engine.py +7 -5
- package/src/skcapstone/sync/models.py +4 -1
- package/src/skcapstone/sync/vault.py +356 -18
- package/src/skcapstone/sync_engine.py +363 -0
- package/src/skcapstone/sync_watcher.py +745 -0
- package/src/skcapstone/systemd.py +331 -0
- package/src/skcapstone/team_comms.py +476 -0
- package/src/skcapstone/team_engine.py +522 -0
- package/src/skcapstone/testrunner.py +300 -0
- package/src/skcapstone/tls.py +150 -0
- package/src/skcapstone/tokens.py +5 -5
- package/src/skcapstone/trust_calibration.py +202 -0
- package/src/skcapstone/trust_graph.py +449 -0
- package/src/skcapstone/trustee_monitor.py +385 -0
- package/src/skcapstone/trustee_ops.py +425 -0
- package/src/skcapstone/unified_search.py +421 -0
- package/src/skcapstone/uninstall_wizard.py +694 -0
- package/src/skcapstone/usage.py +331 -0
- package/src/skcapstone/version_check.py +148 -0
- package/src/skcapstone/warmth_anchor.py +333 -0
- package/src/skcapstone/whoami.py +294 -0
- package/systemd/skcapstone-api.socket +9 -0
- package/systemd/skcapstone-memory-compress.service +18 -0
- package/systemd/skcapstone-memory-compress.timer +11 -0
- package/systemd/skcapstone.service +36 -0
- package/systemd/skcapstone@.service +50 -0
- package/systemd/skcomm-heartbeat.service +18 -0
- package/systemd/skcomm-heartbeat.timer +12 -0
- package/systemd/skcomm-queue-drain.service +17 -0
- package/systemd/skcomm-queue-drain.timer +12 -0
- package/tests/conftest.py +13 -1
- package/tests/integration/__init__.py +1 -0
- package/tests/integration/test_consciousness_e2e.py +877 -0
- package/tests/integration/test_skills_registry.py +744 -0
- package/tests/test_agent_card.py +190 -0
- package/tests/test_agent_runtime.py +1283 -0
- package/tests/test_alerts_cmd.py +291 -0
- package/tests/test_archiver.py +498 -0
- package/tests/test_backup.py +254 -0
- package/tests/test_benchmark.py +366 -0
- package/tests/test_blueprints.py +457 -0
- package/tests/test_capabilities.py +257 -0
- package/tests/test_changelog.py +254 -0
- package/tests/test_chat.py +385 -0
- package/tests/test_claude_md.py +271 -0
- package/tests/test_cli_chat_llm.py +336 -0
- package/tests/test_cli_completions.py +390 -0
- package/tests/test_cli_init_reset.py +164 -0
- package/tests/test_cli_memory.py +208 -0
- package/tests/test_cli_profile.py +294 -0
- package/tests/test_cli_skills.py +223 -0
- package/tests/test_cli_status.py +395 -0
- package/tests/test_cli_test_cmd.py +206 -0
- package/tests/test_cli_test_connection.py +364 -0
- package/tests/test_cloud9_bridge.py +260 -0
- package/tests/test_cloud_provider.py +449 -0
- package/tests/test_cloud_providers.py +522 -0
- package/tests/test_completions.py +158 -0
- package/tests/test_component_manager.py +398 -0
- package/tests/test_config_reload.py +386 -0
- package/tests/test_config_validate.py +529 -0
- package/tests/test_consciousness_e2e.py +296 -0
- package/tests/test_consciousness_loop.py +1289 -0
- package/tests/test_context_loader.py +310 -0
- package/tests/test_conversation_api.py +306 -0
- package/tests/test_conversation_manager.py +381 -0
- package/tests/test_conversation_store.py +391 -0
- package/tests/test_conversation_summarizer.py +302 -0
- package/tests/test_cross_package.py +791 -0
- package/tests/test_crush_shim.py +519 -0
- package/tests/test_daemon.py +781 -0
- package/tests/test_daemon_shutdown.py +309 -0
- package/tests/test_dashboard.py +454 -0
- package/tests/test_discovery.py +200 -6
- package/tests/test_docker_provider.py +966 -0
- package/tests/test_doctor.py +257 -0
- package/tests/test_doctor_fix.py +351 -0
- package/tests/test_e2e_automated.py +292 -0
- package/tests/test_error_queue.py +404 -0
- package/tests/test_export.py +441 -0
- package/tests/test_fallback_tracker.py +219 -0
- package/tests/test_file_transfer.py +397 -0
- package/tests/test_fuse_mount.py +832 -0
- package/tests/test_health_loop.py +422 -0
- package/tests/test_heartbeat.py +354 -0
- package/tests/test_housekeeping.py +195 -0
- package/tests/test_identity_capauth.py +307 -0
- package/tests/test_identity_pillar.py +117 -0
- package/tests/test_install_wizard.py +68 -0
- package/tests/test_integration.py +325 -0
- package/tests/test_kms.py +495 -0
- package/tests/test_llm_providers.py +265 -0
- package/tests/test_local_provider.py +591 -0
- package/tests/test_log_config.py +199 -0
- package/tests/test_logs_cmd.py +287 -0
- package/tests/test_mcp_server.py +1909 -0
- package/tests/test_memory_adapter.py +339 -0
- package/tests/test_memory_curator.py +218 -0
- package/tests/test_memory_engine.py +6 -0
- package/tests/test_memory_fortress.py +571 -0
- package/tests/test_memory_pillar.py +119 -0
- package/tests/test_memory_promoter.py +445 -0
- package/tests/test_memory_verifier.py +420 -0
- package/tests/test_message_crypto.py +187 -0
- package/tests/test_metrics.py +632 -0
- package/tests/test_migrate_memories.py +464 -0
- package/tests/test_model_router.py +546 -0
- package/tests/test_mood.py +394 -0
- package/tests/test_multi_agent.py +269 -0
- package/tests/test_notifications.py +270 -0
- package/tests/test_onboard.py +500 -0
- package/tests/test_peer_directory.py +395 -0
- package/tests/test_peers.py +248 -0
- package/tests/test_pillars.py +87 -9
- package/tests/test_preflight.py +484 -0
- package/tests/test_prompt_adapter.py +331 -0
- package/tests/test_proxmox_provider.py +571 -0
- package/tests/test_pubsub.py +377 -0
- package/tests/test_rate_limiter.py +121 -0
- package/tests/test_registry_client.py +129 -0
- package/tests/test_response_cache.py +312 -0
- package/tests/test_response_scorer.py +294 -0
- package/tests/test_runtime.py +59 -0
- package/tests/test_scheduled_tasks.py +451 -0
- package/tests/test_security.py +250 -0
- package/tests/test_security_pillar.py +213 -0
- package/tests/test_self_healing.py +171 -0
- package/tests/test_session_capture.py +200 -0
- package/tests/test_session_recorder.py +360 -0
- package/tests/test_session_skills.py +235 -0
- package/tests/test_shell.py +210 -0
- package/tests/test_snapshots.py +549 -0
- package/tests/test_soul.py +984 -0
- package/tests/test_soul_swap.py +406 -0
- package/tests/test_spawner.py +211 -0
- package/tests/test_state_diff.py +173 -0
- package/tests/test_summary.py +135 -0
- package/tests/test_sync.py +315 -5
- package/tests/test_sync_backends.py +560 -0
- package/tests/test_sync_engine.py +482 -0
- package/tests/test_sync_pillar.py +344 -0
- package/tests/test_sync_pipeline.py +364 -0
- package/tests/test_sync_vault.py +581 -0
- package/tests/test_syncthing_setup.py +168 -22
- package/tests/test_systemd.py +323 -0
- package/tests/test_team_comms.py +408 -0
- package/tests/test_team_engine.py +397 -0
- package/tests/test_testrunner.py +238 -0
- package/tests/test_trust_calibration.py +204 -0
- package/tests/test_trust_graph.py +207 -0
- package/tests/test_trust_pillar.py +291 -0
- package/tests/test_trustee_cli.py +427 -0
- package/tests/test_trustee_cli_integration.py +325 -0
- package/tests/test_trustee_monitor.py +394 -0
- package/tests/test_trustee_ops.py +355 -0
- package/tests/test_unified_search.py +363 -0
- package/tests/test_uninstall_wizard.py +193 -0
- package/tests/test_usage.py +333 -0
- package/tests/test_version_cmd.py +355 -0
- package/tests/test_warmth_anchor.py +162 -0
- package/tests/test_whoami.py +245 -0
- package/tests/test_ws.py +311 -0
- package/.cursorrules +0 -33
- package/src/skcapstone/cli.py +0 -1441
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Peer Directory — transport address map for the sovereignty mesh.
|
|
3
|
+
|
|
4
|
+
Maps agent names to their SKComm transport addresses (Syncthing outbox
|
|
5
|
+
paths, WebRTC fingerprints, Tailscale IPs, etc.).
|
|
6
|
+
|
|
7
|
+
Separate from PeerRecord (PGP identity in peers.py) — this module owns
|
|
8
|
+
the *routing* layer, not the trust/cryptography layer.
|
|
9
|
+
|
|
10
|
+
Storage: {skcapstone_home}/peers/directory.yaml
|
|
11
|
+
|
|
12
|
+
Entry format:
|
|
13
|
+
lumina:
|
|
14
|
+
address: /home/user/.skcapstone/sync/comms/outbox/lumina
|
|
15
|
+
transport: syncthing
|
|
16
|
+
fingerprint: ABCD1234...
|
|
17
|
+
last_seen: 2026-03-02T...
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
import yaml
|
|
29
|
+
from pydantic import BaseModel, Field
|
|
30
|
+
|
|
31
|
+
from . import SHARED_ROOT
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger("skcapstone.peer_directory")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Model
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DirectoryEntry(BaseModel):
|
|
42
|
+
"""A single peer's transport routing entry.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
name: Normalized peer name (lowercase).
|
|
46
|
+
address: Transport address — Syncthing outbox path, Tailscale IP,
|
|
47
|
+
WebRTC fingerprint URI, or other transport-specific locator.
|
|
48
|
+
transport: Transport type label (syncthing, webrtc, tailscale, file).
|
|
49
|
+
fingerprint: Optional PGP fingerprint for cross-referencing.
|
|
50
|
+
last_seen: ISO-8601 UTC timestamp of last known activity.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
address: str
|
|
55
|
+
transport: str = "syncthing"
|
|
56
|
+
fingerprint: str = ""
|
|
57
|
+
last_seen: Optional[str] = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# PeerDirectory
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PeerDirectory:
|
|
66
|
+
"""Transport address directory for the sovereign agent mesh.
|
|
67
|
+
|
|
68
|
+
Maps agent names → transport addresses. Separate from the PGP identity
|
|
69
|
+
peer store (peers.py); this module owns routing, not trust.
|
|
70
|
+
|
|
71
|
+
Storage: {home}/peers/directory.yaml
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
home: skcapstone home directory. Defaults to SHARED_ROOT (~/.skcapstone).
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, home: Optional[Path] = None) -> None:
|
|
78
|
+
self._home = home or Path(SHARED_ROOT).expanduser()
|
|
79
|
+
self._path = self._home / "peers" / "directory.yaml"
|
|
80
|
+
self._entries: dict[str, DirectoryEntry] = {}
|
|
81
|
+
self._loaded = False
|
|
82
|
+
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
# Public API
|
|
85
|
+
# ------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def load(self) -> dict[str, DirectoryEntry]:
|
|
88
|
+
"""Read the directory from disk.
|
|
89
|
+
|
|
90
|
+
Safe to call multiple times — idempotent re-load.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Dict mapping normalized name -> DirectoryEntry.
|
|
94
|
+
"""
|
|
95
|
+
if not self._path.exists():
|
|
96
|
+
self._entries = {}
|
|
97
|
+
self._loaded = True
|
|
98
|
+
return {}
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
raw = yaml.safe_load(self._path.read_text(encoding="utf-8")) or {}
|
|
102
|
+
entries: dict[str, DirectoryEntry] = {}
|
|
103
|
+
for name, entry in raw.items():
|
|
104
|
+
if not isinstance(entry, dict):
|
|
105
|
+
continue
|
|
106
|
+
try:
|
|
107
|
+
entries[str(name).lower()] = DirectoryEntry.model_validate(
|
|
108
|
+
{"name": str(name).lower(), **entry}
|
|
109
|
+
)
|
|
110
|
+
except Exception as exc:
|
|
111
|
+
logger.debug("Skipping malformed entry '%s': %s", name, exc)
|
|
112
|
+
self._entries = entries
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
logger.warning("Failed to load peer directory: %s", exc)
|
|
115
|
+
self._entries = {}
|
|
116
|
+
|
|
117
|
+
self._loaded = True
|
|
118
|
+
return dict(self._entries)
|
|
119
|
+
|
|
120
|
+
def resolve(self, name: str) -> Optional[str]:
|
|
121
|
+
"""Get the transport address for a named peer.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
name: Peer name (case-insensitive).
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Transport address string, or None if the peer is not in the
|
|
128
|
+
directory.
|
|
129
|
+
"""
|
|
130
|
+
self._ensure_loaded()
|
|
131
|
+
entry = self._entries.get(name.lower())
|
|
132
|
+
return entry.address if entry else None
|
|
133
|
+
|
|
134
|
+
def add_peer(
|
|
135
|
+
self,
|
|
136
|
+
name: str,
|
|
137
|
+
address: str,
|
|
138
|
+
transport: str = "syncthing",
|
|
139
|
+
fingerprint: str = "",
|
|
140
|
+
) -> DirectoryEntry:
|
|
141
|
+
"""Add or update a peer's transport entry.
|
|
142
|
+
|
|
143
|
+
If the peer already exists it is overwritten. Persists atomically.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
name: Peer display name (normalised to lowercase as the key).
|
|
147
|
+
address: Transport address (path, IP, URI, etc.).
|
|
148
|
+
transport: Transport type — syncthing, webrtc, tailscale, file.
|
|
149
|
+
fingerprint: Optional PGP fingerprint for cross-referencing.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The created or updated DirectoryEntry.
|
|
153
|
+
"""
|
|
154
|
+
self._ensure_loaded()
|
|
155
|
+
entry = DirectoryEntry(
|
|
156
|
+
name=name.lower(),
|
|
157
|
+
address=address,
|
|
158
|
+
transport=transport,
|
|
159
|
+
fingerprint=fingerprint,
|
|
160
|
+
last_seen=datetime.now(timezone.utc).isoformat(),
|
|
161
|
+
)
|
|
162
|
+
self._entries[name.lower()] = entry
|
|
163
|
+
self._save()
|
|
164
|
+
logger.info("Directory: added '%s' → %s (%s)", name, address, transport)
|
|
165
|
+
return entry
|
|
166
|
+
|
|
167
|
+
def remove_peer(self, name: str) -> bool:
|
|
168
|
+
"""Remove a peer from the directory.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
name: Peer name to remove (case-insensitive).
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
True if the peer was found and removed, False otherwise.
|
|
175
|
+
"""
|
|
176
|
+
self._ensure_loaded()
|
|
177
|
+
key = name.lower()
|
|
178
|
+
if key not in self._entries:
|
|
179
|
+
return False
|
|
180
|
+
del self._entries[key]
|
|
181
|
+
self._save()
|
|
182
|
+
logger.info("Directory: removed '%s'", name)
|
|
183
|
+
return True
|
|
184
|
+
|
|
185
|
+
def list_peers(self) -> list[DirectoryEntry]:
|
|
186
|
+
"""Return all known peers, sorted by name.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
List of DirectoryEntry sorted alphabetically.
|
|
190
|
+
"""
|
|
191
|
+
self._ensure_loaded()
|
|
192
|
+
return sorted(self._entries.values(), key=lambda e: e.name)
|
|
193
|
+
|
|
194
|
+
def update_last_seen(self, name: str) -> None:
|
|
195
|
+
"""Touch the last_seen timestamp for a peer (in-place + save).
|
|
196
|
+
|
|
197
|
+
Called by the consciousness loop whenever a message arrives from
|
|
198
|
+
a known peer, so the directory stays current without a full re-add.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
name: Peer name (case-insensitive). No-op if peer is unknown.
|
|
202
|
+
"""
|
|
203
|
+
self._ensure_loaded()
|
|
204
|
+
key = name.lower()
|
|
205
|
+
if key not in self._entries:
|
|
206
|
+
return
|
|
207
|
+
self._entries[key].last_seen = datetime.now(timezone.utc).isoformat()
|
|
208
|
+
self._save()
|
|
209
|
+
|
|
210
|
+
def auto_discover(
|
|
211
|
+
self,
|
|
212
|
+
heartbeats_dir: Optional[Path] = None,
|
|
213
|
+
) -> list[DirectoryEntry]:
|
|
214
|
+
"""Discover peers from heartbeat files and Syncthing outbox dirs.
|
|
215
|
+
|
|
216
|
+
Scans two sources and adds any *new* peers (existing entries are
|
|
217
|
+
never overwritten):
|
|
218
|
+
|
|
219
|
+
1. ``{home}/heartbeats/*.json`` — live heartbeat files published by
|
|
220
|
+
each agent via HeartbeatBeacon.
|
|
221
|
+
2. ``{home}/sync/comms/outbox/`` — one sub-directory per peer that
|
|
222
|
+
Syncthing keeps in sync.
|
|
223
|
+
|
|
224
|
+
Syncthing outbox path is used as the default address because that
|
|
225
|
+
is where SKComm writes messages for the peer.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
heartbeats_dir: Override for the heartbeats directory. Defaults
|
|
229
|
+
to ``{home}/heartbeats``.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
List of newly-added DirectoryEntry objects (empty if all peers
|
|
233
|
+
were already known).
|
|
234
|
+
"""
|
|
235
|
+
self._ensure_loaded()
|
|
236
|
+
added: list[DirectoryEntry] = []
|
|
237
|
+
|
|
238
|
+
hb_dir = heartbeats_dir or (self._home / "heartbeats")
|
|
239
|
+
|
|
240
|
+
# 1. Scan heartbeat files
|
|
241
|
+
if hb_dir.exists():
|
|
242
|
+
for hb_file in sorted(hb_dir.glob("*.json")):
|
|
243
|
+
if hb_file.name.endswith(".tmp"):
|
|
244
|
+
continue
|
|
245
|
+
agent_name = hb_file.stem.lower()
|
|
246
|
+
if agent_name in self._entries:
|
|
247
|
+
# Still update last_seen from heartbeat timestamp
|
|
248
|
+
try:
|
|
249
|
+
data = json.loads(hb_file.read_text(encoding="utf-8"))
|
|
250
|
+
ts = data.get("timestamp", "")
|
|
251
|
+
if ts:
|
|
252
|
+
self._entries[agent_name].last_seen = ts
|
|
253
|
+
except Exception:
|
|
254
|
+
pass
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
data = json.loads(hb_file.read_text(encoding="utf-8"))
|
|
259
|
+
# Default Syncthing outbox path for this peer
|
|
260
|
+
outbox = self._home / "sync" / "comms" / "outbox" / agent_name
|
|
261
|
+
entry = DirectoryEntry(
|
|
262
|
+
name=agent_name,
|
|
263
|
+
address=str(outbox),
|
|
264
|
+
transport="syncthing",
|
|
265
|
+
fingerprint=data.get("fingerprint", ""),
|
|
266
|
+
last_seen=data.get("timestamp", datetime.now(timezone.utc).isoformat()),
|
|
267
|
+
)
|
|
268
|
+
self._entries[agent_name] = entry
|
|
269
|
+
added.append(entry)
|
|
270
|
+
logger.info("Auto-discovered '%s' from heartbeat", agent_name)
|
|
271
|
+
except Exception as exc:
|
|
272
|
+
logger.debug("Cannot parse heartbeat %s: %s", hb_file.name, exc)
|
|
273
|
+
|
|
274
|
+
# 2. Scan Syncthing outbox dirs
|
|
275
|
+
outbox_root = self._home / "sync" / "comms" / "outbox"
|
|
276
|
+
if outbox_root.exists():
|
|
277
|
+
for peer_dir in sorted(outbox_root.iterdir()):
|
|
278
|
+
if not peer_dir.is_dir():
|
|
279
|
+
continue
|
|
280
|
+
agent_name = peer_dir.name.lower()
|
|
281
|
+
if agent_name in self._entries:
|
|
282
|
+
continue
|
|
283
|
+
entry = DirectoryEntry(
|
|
284
|
+
name=agent_name,
|
|
285
|
+
address=str(peer_dir),
|
|
286
|
+
transport="syncthing",
|
|
287
|
+
)
|
|
288
|
+
self._entries[agent_name] = entry
|
|
289
|
+
added.append(entry)
|
|
290
|
+
logger.info("Auto-discovered '%s' from outbox", agent_name)
|
|
291
|
+
|
|
292
|
+
if added:
|
|
293
|
+
self._save()
|
|
294
|
+
|
|
295
|
+
return added
|
|
296
|
+
|
|
297
|
+
# ------------------------------------------------------------------
|
|
298
|
+
# Internal helpers
|
|
299
|
+
# ------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
def _ensure_loaded(self) -> None:
|
|
302
|
+
"""Load from disk on first access."""
|
|
303
|
+
if not self._loaded:
|
|
304
|
+
self.load()
|
|
305
|
+
|
|
306
|
+
def _save(self) -> None:
|
|
307
|
+
"""Atomically serialize the directory to YAML."""
|
|
308
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
309
|
+
|
|
310
|
+
data: dict[str, dict] = {}
|
|
311
|
+
for name, entry in sorted(self._entries.items()):
|
|
312
|
+
row: dict = {
|
|
313
|
+
"address": entry.address,
|
|
314
|
+
"transport": entry.transport,
|
|
315
|
+
}
|
|
316
|
+
if entry.fingerprint:
|
|
317
|
+
row["fingerprint"] = entry.fingerprint
|
|
318
|
+
if entry.last_seen:
|
|
319
|
+
row["last_seen"] = entry.last_seen
|
|
320
|
+
data[name] = row
|
|
321
|
+
|
|
322
|
+
tmp = self._path.with_suffix(".yaml.tmp")
|
|
323
|
+
tmp.write_text(yaml.dump(data, default_flow_style=False), encoding="utf-8")
|
|
324
|
+
tmp.rename(self._path)
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sovereign peer management — the other half of P2P discovery.
|
|
3
|
+
|
|
4
|
+
whoami exports your identity card. This module imports someone
|
|
5
|
+
else's card and registers them as a peer in the SKComm keystore.
|
|
6
|
+
The two together form the complete P2P discovery loop.
|
|
7
|
+
|
|
8
|
+
Flow:
|
|
9
|
+
1. Agent A runs: skcapstone whoami --export card.json
|
|
10
|
+
2. Agent A shares card.json with Agent B (USB, chat, email, QR)
|
|
11
|
+
3. Agent B runs: skcapstone peer add --card card.json
|
|
12
|
+
4. Agent B can now send encrypted messages to Agent A
|
|
13
|
+
|
|
14
|
+
Peer data is stored at:
|
|
15
|
+
~/.skcomm/peers/<name>.yml — SKComm peer config
|
|
16
|
+
~/.skcapstone/peers/<name>.json — Extended peer metadata
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
import yaml
|
|
28
|
+
from pydantic import BaseModel, Field
|
|
29
|
+
|
|
30
|
+
from . import SHARED_ROOT
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("skcapstone.peers")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PeerRecord(BaseModel):
|
|
36
|
+
"""A known peer agent.
|
|
37
|
+
|
|
38
|
+
Attributes:
|
|
39
|
+
name: Peer display name.
|
|
40
|
+
fingerprint: CapAuth PGP fingerprint.
|
|
41
|
+
public_key: ASCII-armored PGP public key.
|
|
42
|
+
entity_type: human, ai, or organization.
|
|
43
|
+
handle: CapAuth identity handle.
|
|
44
|
+
email: Contact email.
|
|
45
|
+
capabilities: What this peer can do.
|
|
46
|
+
contact_uris: How to reach this peer.
|
|
47
|
+
trust_level: verified, trusted, sovereign, or unknown.
|
|
48
|
+
added_at: When the peer was added.
|
|
49
|
+
last_seen: Last known activity.
|
|
50
|
+
source: How we learned about this peer (card, discovery, manual).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
name: str
|
|
54
|
+
fingerprint: str = ""
|
|
55
|
+
public_key: str = ""
|
|
56
|
+
entity_type: str = "unknown"
|
|
57
|
+
handle: str = ""
|
|
58
|
+
email: str = ""
|
|
59
|
+
capabilities: list[str] = Field(default_factory=list)
|
|
60
|
+
contact_uris: list[str] = Field(default_factory=list)
|
|
61
|
+
trust_level: str = "unknown"
|
|
62
|
+
added_at: str = Field(
|
|
63
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
64
|
+
)
|
|
65
|
+
last_seen: Optional[str] = None
|
|
66
|
+
source: str = "manual"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def add_peer_from_card(
|
|
70
|
+
card_path: Path,
|
|
71
|
+
skcapstone_home: Optional[Path] = None,
|
|
72
|
+
skcomm_home: Optional[Path] = None,
|
|
73
|
+
) -> PeerRecord:
|
|
74
|
+
"""Import a peer from a whoami identity card.
|
|
75
|
+
|
|
76
|
+
Reads the card JSON, creates peer records in both skcapstone
|
|
77
|
+
and skcomm directories, and writes the public key for SKComm
|
|
78
|
+
encryption.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
card_path: Path to the exported card.json.
|
|
82
|
+
skcapstone_home: Override skcapstone home. Defaults to ~/.skcapstone/.
|
|
83
|
+
skcomm_home: Override skcomm home. Defaults to ~/.skcomm/.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
PeerRecord: The registered peer.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
FileNotFoundError: If the card file doesn't exist.
|
|
90
|
+
ValueError: If the card is missing required fields.
|
|
91
|
+
"""
|
|
92
|
+
card_path = Path(card_path)
|
|
93
|
+
if not card_path.exists():
|
|
94
|
+
raise FileNotFoundError(f"Card not found: {card_path}")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
card_data = json.loads(card_path.read_text(encoding="utf-8"))
|
|
98
|
+
except json.JSONDecodeError as exc:
|
|
99
|
+
raise ValueError(f"Invalid card JSON: {exc}") from exc
|
|
100
|
+
|
|
101
|
+
name = card_data.get("name", "").strip()
|
|
102
|
+
if not name:
|
|
103
|
+
raise ValueError("Card is missing a 'name' field")
|
|
104
|
+
|
|
105
|
+
peer = PeerRecord(
|
|
106
|
+
name=name,
|
|
107
|
+
fingerprint=card_data.get("fingerprint", ""),
|
|
108
|
+
public_key=card_data.get("public_key", ""),
|
|
109
|
+
entity_type=card_data.get("entity_type", "unknown"),
|
|
110
|
+
handle=card_data.get("handle", ""),
|
|
111
|
+
email=card_data.get("email", ""),
|
|
112
|
+
capabilities=card_data.get("capabilities", []),
|
|
113
|
+
contact_uris=card_data.get("contact_uris", []),
|
|
114
|
+
trust_level="verified",
|
|
115
|
+
source="card",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
sk_home = skcapstone_home or Path(SHARED_ROOT).expanduser()
|
|
119
|
+
sc_home = skcomm_home or Path.home() / ".skcomm"
|
|
120
|
+
|
|
121
|
+
_save_skcapstone_peer(sk_home, peer)
|
|
122
|
+
_save_skcomm_peer(sc_home, peer)
|
|
123
|
+
|
|
124
|
+
logger.info("Added peer '%s' (fingerprint: %s)", name, peer.fingerprint[:16])
|
|
125
|
+
return peer
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def add_peer_manual(
|
|
129
|
+
name: str,
|
|
130
|
+
fingerprint: str = "",
|
|
131
|
+
public_key_path: Optional[Path] = None,
|
|
132
|
+
email: str = "",
|
|
133
|
+
skcapstone_home: Optional[Path] = None,
|
|
134
|
+
skcomm_home: Optional[Path] = None,
|
|
135
|
+
) -> PeerRecord:
|
|
136
|
+
"""Add a peer manually by name and optional key file.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
name: Peer display name.
|
|
140
|
+
fingerprint: PGP fingerprint (optional).
|
|
141
|
+
public_key_path: Path to a .asc public key file (optional).
|
|
142
|
+
email: Contact email (optional).
|
|
143
|
+
skcapstone_home: Override skcapstone home.
|
|
144
|
+
skcomm_home: Override skcomm home.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
PeerRecord: The registered peer.
|
|
148
|
+
"""
|
|
149
|
+
public_key = ""
|
|
150
|
+
if public_key_path and Path(public_key_path).exists():
|
|
151
|
+
public_key = Path(public_key_path).read_text(encoding="utf-8").strip()
|
|
152
|
+
|
|
153
|
+
peer = PeerRecord(
|
|
154
|
+
name=name,
|
|
155
|
+
fingerprint=fingerprint,
|
|
156
|
+
public_key=public_key,
|
|
157
|
+
email=email,
|
|
158
|
+
source="manual",
|
|
159
|
+
trust_level="verified" if public_key else "unknown",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
sk_home = skcapstone_home or Path(SHARED_ROOT).expanduser()
|
|
163
|
+
sc_home = skcomm_home or Path.home() / ".skcomm"
|
|
164
|
+
|
|
165
|
+
_save_skcapstone_peer(sk_home, peer)
|
|
166
|
+
_save_skcomm_peer(sc_home, peer)
|
|
167
|
+
|
|
168
|
+
return peer
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def list_peers(
|
|
172
|
+
skcapstone_home: Optional[Path] = None,
|
|
173
|
+
) -> list[PeerRecord]:
|
|
174
|
+
"""List all known peers.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
skcapstone_home: Override skcapstone home.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
list[PeerRecord]: All registered peers.
|
|
181
|
+
"""
|
|
182
|
+
sk_home = skcapstone_home or Path(SHARED_ROOT).expanduser()
|
|
183
|
+
peers_dir = sk_home / "peers"
|
|
184
|
+
if not peers_dir.exists():
|
|
185
|
+
return []
|
|
186
|
+
|
|
187
|
+
peers = []
|
|
188
|
+
for f in sorted(peers_dir.glob("*.json")):
|
|
189
|
+
try:
|
|
190
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
191
|
+
peers.append(PeerRecord.model_validate(data))
|
|
192
|
+
except (json.JSONDecodeError, Exception):
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
return peers
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_peer(
|
|
199
|
+
name: str,
|
|
200
|
+
skcapstone_home: Optional[Path] = None,
|
|
201
|
+
) -> Optional[PeerRecord]:
|
|
202
|
+
"""Get a specific peer by name.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
name: Peer name to look up.
|
|
206
|
+
skcapstone_home: Override skcapstone home.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
PeerRecord or None if not found.
|
|
210
|
+
"""
|
|
211
|
+
sk_home = skcapstone_home or Path(SHARED_ROOT).expanduser()
|
|
212
|
+
peer_file = sk_home / "peers" / f"{_safe_filename(name)}.json"
|
|
213
|
+
if not peer_file.exists():
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
data = json.loads(peer_file.read_text(encoding="utf-8"))
|
|
218
|
+
return PeerRecord.model_validate(data)
|
|
219
|
+
except (json.JSONDecodeError, Exception):
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def remove_peer(
|
|
224
|
+
name: str,
|
|
225
|
+
skcapstone_home: Optional[Path] = None,
|
|
226
|
+
skcomm_home: Optional[Path] = None,
|
|
227
|
+
) -> bool:
|
|
228
|
+
"""Remove a peer from both skcapstone and skcomm registries.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
name: Peer name to remove.
|
|
232
|
+
skcapstone_home: Override skcapstone home.
|
|
233
|
+
skcomm_home: Override skcomm home.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
bool: True if the peer was found and removed.
|
|
237
|
+
"""
|
|
238
|
+
sk_home = skcapstone_home or Path(SHARED_ROOT).expanduser()
|
|
239
|
+
sc_home = skcomm_home or Path.home() / ".skcomm"
|
|
240
|
+
safe = _safe_filename(name)
|
|
241
|
+
removed = False
|
|
242
|
+
|
|
243
|
+
sk_file = sk_home / "peers" / f"{safe}.json"
|
|
244
|
+
if sk_file.exists():
|
|
245
|
+
sk_file.unlink()
|
|
246
|
+
removed = True
|
|
247
|
+
|
|
248
|
+
sc_file = sc_home / "peers" / f"{safe}.yml"
|
|
249
|
+
if sc_file.exists():
|
|
250
|
+
sc_file.unlink()
|
|
251
|
+
removed = True
|
|
252
|
+
|
|
253
|
+
sc_key = sc_home / "peers" / f"{safe}.pub.asc"
|
|
254
|
+
if sc_key.exists():
|
|
255
|
+
sc_key.unlink()
|
|
256
|
+
|
|
257
|
+
logger.info("Removed peer '%s'", name)
|
|
258
|
+
return removed
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _save_skcapstone_peer(home: Path, peer: PeerRecord) -> Path:
|
|
262
|
+
"""Save peer record to skcapstone peers directory.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
home: skcapstone home directory.
|
|
266
|
+
peer: Peer to save.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Path: Written file path.
|
|
270
|
+
"""
|
|
271
|
+
peers_dir = home / "peers"
|
|
272
|
+
peers_dir.mkdir(parents=True, exist_ok=True)
|
|
273
|
+
|
|
274
|
+
path = peers_dir / f"{_safe_filename(peer.name)}.json"
|
|
275
|
+
path.write_text(peer.model_dump_json(indent=2), encoding="utf-8")
|
|
276
|
+
return path
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _save_skcomm_peer(home: Path, peer: PeerRecord) -> Path:
|
|
280
|
+
"""Save peer to SKComm peers directory (YAML + public key).
|
|
281
|
+
|
|
282
|
+
Creates the YAML config that SKComm's KeyStore reads, and
|
|
283
|
+
writes the public key as a separate .asc file.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
home: skcomm home directory.
|
|
287
|
+
peer: Peer to save.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Path: Written YAML file path.
|
|
291
|
+
"""
|
|
292
|
+
peers_dir = home / "peers"
|
|
293
|
+
peers_dir.mkdir(parents=True, exist_ok=True)
|
|
294
|
+
|
|
295
|
+
safe = _safe_filename(peer.name)
|
|
296
|
+
|
|
297
|
+
if peer.public_key:
|
|
298
|
+
key_path = peers_dir / f"{safe}.pub.asc"
|
|
299
|
+
key_path.write_text(peer.public_key, encoding="utf-8")
|
|
300
|
+
pubkey_ref = str(key_path)
|
|
301
|
+
else:
|
|
302
|
+
pubkey_ref = ""
|
|
303
|
+
|
|
304
|
+
peer_yml = {
|
|
305
|
+
"name": peer.name,
|
|
306
|
+
"fingerprint": peer.fingerprint,
|
|
307
|
+
"public_key": pubkey_ref,
|
|
308
|
+
"email": peer.email,
|
|
309
|
+
"trust_level": peer.trust_level,
|
|
310
|
+
"added_at": peer.added_at,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
path = peers_dir / f"{safe}.yml"
|
|
314
|
+
path.write_text(yaml.dump(peer_yml, default_flow_style=False), encoding="utf-8")
|
|
315
|
+
return path
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _safe_filename(name: str) -> str:
|
|
319
|
+
"""Convert a peer name to a safe filename.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
name: Peer display name.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
str: Filesystem-safe version of the name.
|
|
326
|
+
"""
|
|
327
|
+
safe = name.lower().strip().replace(" ", "-")
|
|
328
|
+
safe = "".join(c for c in safe if c.isalnum() or c in "-_.")
|
|
329
|
+
return safe or "unnamed"
|