@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,345 @@
|
|
|
1
|
+
"""Trustee operations tools (health, restart, scale, rotate, monitor, logs, deployments)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from mcp.types import TextContent, Tool
|
|
6
|
+
|
|
7
|
+
from ._helpers import _error_response, _home, _json_response
|
|
8
|
+
|
|
9
|
+
TOOLS: list[Tool] = [
|
|
10
|
+
Tool(
|
|
11
|
+
name="trustee_health",
|
|
12
|
+
description=(
|
|
13
|
+
"Run health checks on all agents in a deployment. "
|
|
14
|
+
"Returns per-agent status, heartbeat, and error info."
|
|
15
|
+
),
|
|
16
|
+
inputSchema={
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"deployment_id": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"description": "The deployment ID to check",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
"required": ["deployment_id"],
|
|
25
|
+
},
|
|
26
|
+
),
|
|
27
|
+
Tool(
|
|
28
|
+
name="trustee_restart",
|
|
29
|
+
description=(
|
|
30
|
+
"Restart a failed agent or all agents in a deployment. "
|
|
31
|
+
"Calls provider stop/start and updates deployment state."
|
|
32
|
+
),
|
|
33
|
+
inputSchema={
|
|
34
|
+
"type": "object",
|
|
35
|
+
"properties": {
|
|
36
|
+
"deployment_id": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"description": "The deployment ID",
|
|
39
|
+
},
|
|
40
|
+
"agent_name": {
|
|
41
|
+
"type": "string",
|
|
42
|
+
"description": "Agent to restart (omit for all agents)",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
"required": ["deployment_id"],
|
|
46
|
+
},
|
|
47
|
+
),
|
|
48
|
+
Tool(
|
|
49
|
+
name="trustee_scale",
|
|
50
|
+
description=(
|
|
51
|
+
"Scale the number of instances for an agent type up or down. "
|
|
52
|
+
"Adds or removes instances while updating deployment state."
|
|
53
|
+
),
|
|
54
|
+
inputSchema={
|
|
55
|
+
"type": "object",
|
|
56
|
+
"properties": {
|
|
57
|
+
"deployment_id": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "The deployment ID",
|
|
60
|
+
},
|
|
61
|
+
"agent_spec_key": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "The agent spec key (role) to scale",
|
|
64
|
+
},
|
|
65
|
+
"count": {
|
|
66
|
+
"type": "integer",
|
|
67
|
+
"description": "Desired total instance count (>= 1)",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
"required": ["deployment_id", "agent_spec_key", "count"],
|
|
71
|
+
},
|
|
72
|
+
),
|
|
73
|
+
Tool(
|
|
74
|
+
name="trustee_rotate",
|
|
75
|
+
description=(
|
|
76
|
+
"Snapshot context, destroy, and redeploy an agent fresh. "
|
|
77
|
+
"Used when an agent shows context degradation."
|
|
78
|
+
),
|
|
79
|
+
inputSchema={
|
|
80
|
+
"type": "object",
|
|
81
|
+
"properties": {
|
|
82
|
+
"deployment_id": {
|
|
83
|
+
"type": "string",
|
|
84
|
+
"description": "The deployment ID",
|
|
85
|
+
},
|
|
86
|
+
"agent_name": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"description": "Agent to rotate",
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
"required": ["deployment_id", "agent_name"],
|
|
92
|
+
},
|
|
93
|
+
),
|
|
94
|
+
Tool(
|
|
95
|
+
name="trustee_monitor",
|
|
96
|
+
description=(
|
|
97
|
+
"Run a single autonomous monitoring pass over all deployments "
|
|
98
|
+
"or a specific one. Detects stale heartbeats, triggers "
|
|
99
|
+
"auto-restart/rotate, and escalates on critical degradation."
|
|
100
|
+
),
|
|
101
|
+
inputSchema={
|
|
102
|
+
"type": "object",
|
|
103
|
+
"properties": {
|
|
104
|
+
"deployment_id": {
|
|
105
|
+
"type": "string",
|
|
106
|
+
"description": "Specific deployment to check (omit for all)",
|
|
107
|
+
},
|
|
108
|
+
"heartbeat_timeout": {
|
|
109
|
+
"type": "number",
|
|
110
|
+
"description": "Seconds before heartbeat is stale (default: 120)",
|
|
111
|
+
},
|
|
112
|
+
"auto_restart": {
|
|
113
|
+
"type": "boolean",
|
|
114
|
+
"description": "Enable auto-restart on failure (default: true)",
|
|
115
|
+
},
|
|
116
|
+
"auto_rotate": {
|
|
117
|
+
"type": "boolean",
|
|
118
|
+
"description": "Enable auto-rotate after repeated failures (default: true)",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
"required": [],
|
|
122
|
+
},
|
|
123
|
+
),
|
|
124
|
+
Tool(
|
|
125
|
+
name="trustee_logs",
|
|
126
|
+
description=(
|
|
127
|
+
"Get recent log lines for agents in a deployment. "
|
|
128
|
+
"Reads agent log files or falls back to audit log entries."
|
|
129
|
+
),
|
|
130
|
+
inputSchema={
|
|
131
|
+
"type": "object",
|
|
132
|
+
"properties": {
|
|
133
|
+
"deployment_id": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"description": "The deployment ID",
|
|
136
|
+
},
|
|
137
|
+
"agent_name": {
|
|
138
|
+
"type": "string",
|
|
139
|
+
"description": "Specific agent (omit for all)",
|
|
140
|
+
},
|
|
141
|
+
"tail": {
|
|
142
|
+
"type": "integer",
|
|
143
|
+
"description": "Max lines per agent (default: 50)",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
"required": ["deployment_id"],
|
|
147
|
+
},
|
|
148
|
+
),
|
|
149
|
+
Tool(
|
|
150
|
+
name="trustee_deployments",
|
|
151
|
+
description=(
|
|
152
|
+
"List all active deployments with agent counts and status. "
|
|
153
|
+
"Overview of the entire team fleet."
|
|
154
|
+
),
|
|
155
|
+
inputSchema={"type": "object", "properties": {}, "required": []},
|
|
156
|
+
),
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ── Helpers ──────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _get_trustee_ops():
|
|
164
|
+
"""Build TrusteeOps and TeamEngine from agent home."""
|
|
165
|
+
from ..team_engine import TeamEngine
|
|
166
|
+
from ..trustee_ops import TrusteeOps
|
|
167
|
+
|
|
168
|
+
home = _home()
|
|
169
|
+
engine = TeamEngine(home=home, provider=None, comms_root=None)
|
|
170
|
+
ops = TrusteeOps(engine=engine, home=home)
|
|
171
|
+
return ops, engine
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ── Handlers ─────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def _handle_trustee_health(args: dict) -> list[TextContent]:
|
|
178
|
+
"""Run health checks on a deployment."""
|
|
179
|
+
deployment_id = args.get("deployment_id", "")
|
|
180
|
+
if not deployment_id:
|
|
181
|
+
return _error_response("deployment_id is required")
|
|
182
|
+
|
|
183
|
+
ops, _ = _get_trustee_ops()
|
|
184
|
+
try:
|
|
185
|
+
report = ops.health_report(deployment_id)
|
|
186
|
+
healthy = sum(1 for r in report if r["healthy"])
|
|
187
|
+
return _json_response({
|
|
188
|
+
"deployment_id": deployment_id,
|
|
189
|
+
"agents": report,
|
|
190
|
+
"summary": {
|
|
191
|
+
"total": len(report),
|
|
192
|
+
"healthy": healthy,
|
|
193
|
+
"degraded": len(report) - healthy,
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
except ValueError as exc:
|
|
197
|
+
return _error_response(str(exc))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def _handle_trustee_restart(args: dict) -> list[TextContent]:
|
|
201
|
+
"""Restart agents in a deployment."""
|
|
202
|
+
deployment_id = args.get("deployment_id", "")
|
|
203
|
+
if not deployment_id:
|
|
204
|
+
return _error_response("deployment_id is required")
|
|
205
|
+
|
|
206
|
+
agent_name = args.get("agent_name")
|
|
207
|
+
ops, _ = _get_trustee_ops()
|
|
208
|
+
try:
|
|
209
|
+
results = ops.restart_agent(deployment_id, agent_name)
|
|
210
|
+
return _json_response({
|
|
211
|
+
"deployment_id": deployment_id,
|
|
212
|
+
"results": results,
|
|
213
|
+
"all_restarted": all(v == "restarted" for v in results.values()),
|
|
214
|
+
})
|
|
215
|
+
except ValueError as exc:
|
|
216
|
+
return _error_response(str(exc))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
async def _handle_trustee_scale(args: dict) -> list[TextContent]:
|
|
220
|
+
"""Scale agent instances in a deployment."""
|
|
221
|
+
deployment_id = args.get("deployment_id", "")
|
|
222
|
+
agent_spec_key = args.get("agent_spec_key", "")
|
|
223
|
+
count = args.get("count", 0)
|
|
224
|
+
if not deployment_id or not agent_spec_key or not count:
|
|
225
|
+
return _error_response("deployment_id, agent_spec_key, and count are required")
|
|
226
|
+
|
|
227
|
+
ops, _ = _get_trustee_ops()
|
|
228
|
+
try:
|
|
229
|
+
result = ops.scale_agent(deployment_id, agent_spec_key, count)
|
|
230
|
+
return _json_response({
|
|
231
|
+
"deployment_id": deployment_id,
|
|
232
|
+
"agent_spec_key": agent_spec_key,
|
|
233
|
+
**result,
|
|
234
|
+
})
|
|
235
|
+
except ValueError as exc:
|
|
236
|
+
return _error_response(str(exc))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def _handle_trustee_rotate(args: dict) -> list[TextContent]:
|
|
240
|
+
"""Rotate an agent (snapshot + fresh deploy)."""
|
|
241
|
+
deployment_id = args.get("deployment_id", "")
|
|
242
|
+
agent_name = args.get("agent_name", "")
|
|
243
|
+
if not deployment_id or not agent_name:
|
|
244
|
+
return _error_response("deployment_id and agent_name are required")
|
|
245
|
+
|
|
246
|
+
ops, _ = _get_trustee_ops()
|
|
247
|
+
try:
|
|
248
|
+
result = ops.rotate_agent(deployment_id, agent_name)
|
|
249
|
+
return _json_response({
|
|
250
|
+
"deployment_id": deployment_id,
|
|
251
|
+
"agent_name": agent_name,
|
|
252
|
+
**result,
|
|
253
|
+
})
|
|
254
|
+
except ValueError as exc:
|
|
255
|
+
return _error_response(str(exc))
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def _handle_trustee_monitor(args: dict) -> list[TextContent]:
|
|
259
|
+
"""Run a single monitoring pass."""
|
|
260
|
+
from ..trustee_monitor import MonitorConfig, TrusteeMonitor
|
|
261
|
+
|
|
262
|
+
ops, engine = _get_trustee_ops()
|
|
263
|
+
config = MonitorConfig(
|
|
264
|
+
heartbeat_timeout=args.get("heartbeat_timeout", 120.0),
|
|
265
|
+
auto_restart=args.get("auto_restart", True),
|
|
266
|
+
auto_rotate=args.get("auto_rotate", True),
|
|
267
|
+
)
|
|
268
|
+
monitor = TrusteeMonitor(ops, engine, config)
|
|
269
|
+
|
|
270
|
+
deployment_id = args.get("deployment_id")
|
|
271
|
+
if deployment_id:
|
|
272
|
+
deployment = engine.get_deployment(deployment_id)
|
|
273
|
+
if not deployment:
|
|
274
|
+
return _error_response(f"Deployment '{deployment_id}' not found")
|
|
275
|
+
report = monitor.check_deployment(deployment)
|
|
276
|
+
else:
|
|
277
|
+
report = monitor.check_all()
|
|
278
|
+
|
|
279
|
+
return _json_response({
|
|
280
|
+
"timestamp": report.timestamp,
|
|
281
|
+
"deployments_checked": report.deployments_checked,
|
|
282
|
+
"agents_healthy": report.agents_healthy,
|
|
283
|
+
"agents_degraded": report.agents_degraded,
|
|
284
|
+
"restarts_triggered": report.restarts_triggered,
|
|
285
|
+
"rotations_triggered": report.rotations_triggered,
|
|
286
|
+
"escalations_sent": report.escalations_sent,
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def _handle_trustee_logs(args: dict) -> list[TextContent]:
|
|
291
|
+
"""Get agent logs from a deployment."""
|
|
292
|
+
deployment_id = args.get("deployment_id", "")
|
|
293
|
+
if not deployment_id:
|
|
294
|
+
return _error_response("deployment_id is required")
|
|
295
|
+
|
|
296
|
+
agent_name = args.get("agent_name")
|
|
297
|
+
tail = args.get("tail", 50)
|
|
298
|
+
ops, _ = _get_trustee_ops()
|
|
299
|
+
try:
|
|
300
|
+
logs = ops.get_logs(deployment_id, agent_name, tail=tail)
|
|
301
|
+
return _json_response({
|
|
302
|
+
"deployment_id": deployment_id,
|
|
303
|
+
"agents": {name: lines for name, lines in logs.items()},
|
|
304
|
+
})
|
|
305
|
+
except ValueError as exc:
|
|
306
|
+
return _error_response(str(exc))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
async def _handle_trustee_deployments(_args: dict) -> list[TextContent]:
|
|
310
|
+
"""List all active deployments."""
|
|
311
|
+
_, engine = _get_trustee_ops()
|
|
312
|
+
deployments = engine.list_deployments()
|
|
313
|
+
return _json_response({
|
|
314
|
+
"count": len(deployments),
|
|
315
|
+
"deployments": [
|
|
316
|
+
{
|
|
317
|
+
"deployment_id": d.deployment_id,
|
|
318
|
+
"blueprint_slug": d.blueprint_slug,
|
|
319
|
+
"team_name": d.team_name,
|
|
320
|
+
"provider": d.provider,
|
|
321
|
+
"status": d.status,
|
|
322
|
+
"agent_count": len(d.agents),
|
|
323
|
+
"agents": {
|
|
324
|
+
name: {
|
|
325
|
+
"status": a.status.value if hasattr(a.status, "value") else str(a.status),
|
|
326
|
+
"host": a.host or "\u2014",
|
|
327
|
+
"last_heartbeat": a.last_heartbeat or "\u2014",
|
|
328
|
+
}
|
|
329
|
+
for name, a in d.agents.items()
|
|
330
|
+
},
|
|
331
|
+
}
|
|
332
|
+
for d in deployments
|
|
333
|
+
],
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
HANDLERS: dict = {
|
|
338
|
+
"trustee_health": _handle_trustee_health,
|
|
339
|
+
"trustee_restart": _handle_trustee_restart,
|
|
340
|
+
"trustee_scale": _handle_trustee_scale,
|
|
341
|
+
"trustee_rotate": _handle_trustee_rotate,
|
|
342
|
+
"trustee_monitor": _handle_trustee_monitor,
|
|
343
|
+
"trustee_logs": _handle_trustee_logs,
|
|
344
|
+
"trustee_deployments": _handle_trustee_deployments,
|
|
345
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mDNS peer discovery for SKCapstone — Zeroconf-based LAN peer detection.
|
|
3
|
+
|
|
4
|
+
Registers ``_skcapstone._tcp`` on daemon start and browses for other
|
|
5
|
+
instances on the local network. Discovered peers are written as synthetic
|
|
6
|
+
heartbeat files (``metadata.source = "mdns"``) so
|
|
7
|
+
``HeartbeatBeacon.discover_peers()`` picks them up through the normal flow.
|
|
8
|
+
|
|
9
|
+
Gracefully disabled at import time if the ``zeroconf`` package is not
|
|
10
|
+
installed — no hard dependency.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import platform as _platform
|
|
18
|
+
import socket
|
|
19
|
+
import threading
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("skcapstone.mdns_discovery")
|
|
25
|
+
|
|
26
|
+
MDNS_SERVICE_TYPE = "_skcapstone._tcp.local."
|
|
27
|
+
|
|
28
|
+
# Short TTL so a stale mDNS peer auto-expires within 2 minutes if the
|
|
29
|
+
# browse callback never fires ``remove_service``.
|
|
30
|
+
MDNS_TTL = 120
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
from zeroconf import ServiceBrowser, ServiceInfo, Zeroconf # type: ignore[import-untyped]
|
|
34
|
+
|
|
35
|
+
_ZEROCONF_AVAILABLE = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
_ZEROCONF_AVAILABLE = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MDNSDiscovery:
|
|
41
|
+
"""mDNS peer discovery for the SKCapstone agent mesh.
|
|
42
|
+
|
|
43
|
+
Registers the local agent as a ``_skcapstone._tcp`` Zeroconf service and
|
|
44
|
+
browses for other instances on the same LAN segment. When a peer is
|
|
45
|
+
found, a synthetic heartbeat JSON file is written to *heartbeats_dir* with
|
|
46
|
+
``metadata.source = "mdns"`` so ``HeartbeatBeacon.discover_peers()``
|
|
47
|
+
transparently includes it. When the service disappears the heartbeat is
|
|
48
|
+
marked offline (TTL=0) so it immediately ages out.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
agent_name: Local agent name — used as the mDNS service instance name
|
|
52
|
+
and written into the synthetic heartbeat ``agent_name`` field.
|
|
53
|
+
port: Local HTTP API port advertised in the TXT record.
|
|
54
|
+
heartbeats_dir: Directory that ``HeartbeatBeacon`` reads heartbeat
|
|
55
|
+
files from (``~/.skcapstone/heartbeats/`` by default).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
agent_name: str,
|
|
61
|
+
port: int,
|
|
62
|
+
heartbeats_dir: Path,
|
|
63
|
+
) -> None:
|
|
64
|
+
self._agent_name = agent_name
|
|
65
|
+
self._port = port
|
|
66
|
+
self._heartbeats_dir = heartbeats_dir
|
|
67
|
+
|
|
68
|
+
self._zc: Optional[object] = None # Zeroconf instance
|
|
69
|
+
self._browser: Optional[object] = None # ServiceBrowser
|
|
70
|
+
self._info: Optional[object] = None # ServiceInfo
|
|
71
|
+
|
|
72
|
+
self._lock = threading.Lock()
|
|
73
|
+
# Maps raw mDNS service name → agent_name for peers we track
|
|
74
|
+
self._mdns_peers: dict[str, str] = {}
|
|
75
|
+
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
# Public API
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def start(self) -> None:
|
|
81
|
+
"""Register the local service and start browsing for peers.
|
|
82
|
+
|
|
83
|
+
Does nothing (logs a warning) when ``zeroconf`` is not installed.
|
|
84
|
+
"""
|
|
85
|
+
if not _ZEROCONF_AVAILABLE:
|
|
86
|
+
logger.warning(
|
|
87
|
+
"zeroconf not installed — mDNS peer discovery disabled. "
|
|
88
|
+
"Install with: pip install 'skcapstone[mdns]'"
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
self._heartbeats_dir.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
|
|
94
|
+
zc = Zeroconf()
|
|
95
|
+
self._zc = zc
|
|
96
|
+
|
|
97
|
+
addresses = self._local_addresses()
|
|
98
|
+
props: dict[str, str] = {
|
|
99
|
+
"agent": self._agent_name,
|
|
100
|
+
"platform": f"{_platform.system()} {_platform.machine()}",
|
|
101
|
+
}
|
|
102
|
+
instance_name = f"{self._agent_name}.{MDNS_SERVICE_TYPE}"
|
|
103
|
+
self._info = ServiceInfo(
|
|
104
|
+
type_=MDNS_SERVICE_TYPE,
|
|
105
|
+
name=instance_name,
|
|
106
|
+
addresses=addresses,
|
|
107
|
+
port=self._port,
|
|
108
|
+
properties=props,
|
|
109
|
+
server=f"{socket.gethostname()}.local.",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
zc.register_service(self._info)
|
|
114
|
+
logger.info(
|
|
115
|
+
"mDNS: registered '%s' on port %d", instance_name, self._port
|
|
116
|
+
)
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
logger.warning("mDNS: service registration failed: %s", exc)
|
|
119
|
+
|
|
120
|
+
self._browser = ServiceBrowser(zc, MDNS_SERVICE_TYPE, self._make_listener())
|
|
121
|
+
logger.info("mDNS: browsing for %s", MDNS_SERVICE_TYPE)
|
|
122
|
+
|
|
123
|
+
def stop(self) -> None:
|
|
124
|
+
"""Unregister the local service and close the Zeroconf socket."""
|
|
125
|
+
if self._zc is None:
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
if self._info is not None:
|
|
130
|
+
self._zc.unregister_service(self._info) # type: ignore[attr-defined]
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
logger.debug("mDNS: unregister error: %s", exc)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
self._zc.close() # type: ignore[attr-defined]
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
logger.debug("mDNS: close error: %s", exc)
|
|
138
|
+
|
|
139
|
+
self._zc = None
|
|
140
|
+
logger.info("mDNS: stopped")
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
# Internal
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def _make_listener(self):
|
|
147
|
+
"""Build a zeroconf ServiceListener that delegates to this instance."""
|
|
148
|
+
discovery = self
|
|
149
|
+
|
|
150
|
+
class _Listener:
|
|
151
|
+
def add_service(self, zc, type_: str, name: str) -> None:
|
|
152
|
+
discovery._on_add(zc, type_, name)
|
|
153
|
+
|
|
154
|
+
def remove_service(self, zc, type_: str, name: str) -> None:
|
|
155
|
+
discovery._on_remove(zc, type_, name)
|
|
156
|
+
|
|
157
|
+
def update_service(self, zc, type_: str, name: str) -> None:
|
|
158
|
+
discovery._on_add(zc, type_, name)
|
|
159
|
+
|
|
160
|
+
return _Listener()
|
|
161
|
+
|
|
162
|
+
def _on_add(self, zc, type_: str, name: str) -> None:
|
|
163
|
+
"""Handle a newly discovered or updated ``_skcapstone._tcp`` service."""
|
|
164
|
+
try:
|
|
165
|
+
info = zc.get_service_info(type_, name)
|
|
166
|
+
if info is None:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
props: dict[str, str] = {}
|
|
170
|
+
for k, v in (info.properties or {}).items():
|
|
171
|
+
key = k.decode() if isinstance(k, bytes) else str(k)
|
|
172
|
+
val = v.decode() if isinstance(v, bytes) else str(v)
|
|
173
|
+
props[key] = val
|
|
174
|
+
|
|
175
|
+
agent_name = props.get("agent", name.split(".")[0])
|
|
176
|
+
|
|
177
|
+
# Skip ourselves — our own registration fires the browser too
|
|
178
|
+
if agent_name == self._agent_name:
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
addresses = [
|
|
182
|
+
socket.inet_ntoa(a) if len(a) == 4 else a.hex()
|
|
183
|
+
for a in (info.addresses or [])
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
logger.info(
|
|
187
|
+
"mDNS: discovered peer '%s' at %s:%d",
|
|
188
|
+
agent_name,
|
|
189
|
+
addresses,
|
|
190
|
+
info.port,
|
|
191
|
+
)
|
|
192
|
+
self._write_mdns_heartbeat(agent_name, addresses, info.port, props)
|
|
193
|
+
|
|
194
|
+
with self._lock:
|
|
195
|
+
self._mdns_peers[name] = agent_name
|
|
196
|
+
|
|
197
|
+
except Exception as exc:
|
|
198
|
+
logger.warning("mDNS: error handling add for %s: %s", name, exc)
|
|
199
|
+
|
|
200
|
+
def _on_remove(self, zc, type_: str, name: str) -> None:
|
|
201
|
+
"""Handle a ``_skcapstone._tcp`` service going offline."""
|
|
202
|
+
with self._lock:
|
|
203
|
+
agent_name = self._mdns_peers.pop(name, None)
|
|
204
|
+
|
|
205
|
+
if agent_name is None:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
logger.info("mDNS: peer '%s' left the LAN", agent_name)
|
|
209
|
+
self._write_mdns_heartbeat(agent_name, [], 0, {}, offline=True)
|
|
210
|
+
|
|
211
|
+
def _write_mdns_heartbeat(
|
|
212
|
+
self,
|
|
213
|
+
agent_name: str,
|
|
214
|
+
addresses: list[str],
|
|
215
|
+
port: int,
|
|
216
|
+
props: dict[str, str],
|
|
217
|
+
offline: bool = False,
|
|
218
|
+
) -> None:
|
|
219
|
+
"""Write (or update) a synthetic heartbeat JSON for an mDNS peer.
|
|
220
|
+
|
|
221
|
+
Existing heartbeat files whose ``metadata.source`` is **not** ``mdns``
|
|
222
|
+
are left untouched so a real Syncthing-synced heartbeat is never
|
|
223
|
+
overwritten by a weaker mDNS-sourced one.
|
|
224
|
+
"""
|
|
225
|
+
self._heartbeats_dir.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
|
|
227
|
+
safe_name = agent_name.lower().replace(" ", "-")
|
|
228
|
+
path = self._heartbeats_dir / f"{safe_name}.json"
|
|
229
|
+
|
|
230
|
+
# Guard: do not overwrite a real (non-mDNS) heartbeat
|
|
231
|
+
if path.exists() and not offline:
|
|
232
|
+
try:
|
|
233
|
+
existing = json.loads(path.read_text(encoding="utf-8"))
|
|
234
|
+
if existing.get("metadata", {}).get("source") != "mdns":
|
|
235
|
+
logger.debug(
|
|
236
|
+
"mDNS: skipping heartbeat write for '%s' — "
|
|
237
|
+
"a non-mDNS heartbeat already exists",
|
|
238
|
+
agent_name,
|
|
239
|
+
)
|
|
240
|
+
return
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
heartbeat = {
|
|
245
|
+
"agent_name": agent_name,
|
|
246
|
+
"status": "offline" if offline else "alive",
|
|
247
|
+
"hostname": props.get("hostname", ""),
|
|
248
|
+
"platform": props.get("platform", ""),
|
|
249
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
250
|
+
"ttl_seconds": 0 if offline else MDNS_TTL,
|
|
251
|
+
"uptime_hours": 0.0,
|
|
252
|
+
"soul_active": "",
|
|
253
|
+
"claimed_tasks": [],
|
|
254
|
+
"loaded_model": "",
|
|
255
|
+
"session_active": False,
|
|
256
|
+
"consciousness_active": False,
|
|
257
|
+
"uptime_seconds": 0.0,
|
|
258
|
+
"cpu_load_1min": 0.0,
|
|
259
|
+
"memory_used_mb": 0,
|
|
260
|
+
"active_conversations": 0,
|
|
261
|
+
"messages_processed_24h": 0,
|
|
262
|
+
"capacity": {
|
|
263
|
+
"cpu_count": 0,
|
|
264
|
+
"memory_total_mb": 0,
|
|
265
|
+
"memory_available_mb": 0,
|
|
266
|
+
"disk_free_gb": 0.0,
|
|
267
|
+
"gpu_available": False,
|
|
268
|
+
"gpu_name": "",
|
|
269
|
+
},
|
|
270
|
+
"capabilities": [],
|
|
271
|
+
"version": "",
|
|
272
|
+
"fingerprint": "",
|
|
273
|
+
"metadata": {
|
|
274
|
+
"source": "mdns",
|
|
275
|
+
"addresses": addresses,
|
|
276
|
+
"port": port,
|
|
277
|
+
},
|
|
278
|
+
"services": [],
|
|
279
|
+
"tailscale_ip": "",
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
tmp = path.with_suffix(".json.tmp")
|
|
283
|
+
tmp.write_text(json.dumps(heartbeat, indent=2), encoding="utf-8")
|
|
284
|
+
tmp.rename(path)
|
|
285
|
+
logger.debug(
|
|
286
|
+
"mDNS: wrote heartbeat for '%s' (offline=%s)", agent_name, offline
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def _local_addresses() -> list[bytes]:
|
|
291
|
+
"""Return non-loopback local IPv4 addresses as packed 4-byte values.
|
|
292
|
+
|
|
293
|
+
Falls back to ``127.0.0.1`` if no suitable address is found so that
|
|
294
|
+
the service can still be registered (useful for loopback-only testing).
|
|
295
|
+
"""
|
|
296
|
+
addrs: list[bytes] = []
|
|
297
|
+
try:
|
|
298
|
+
hostname = socket.gethostname()
|
|
299
|
+
for _family, _type, _proto, _canon, sockaddr in socket.getaddrinfo(
|
|
300
|
+
hostname, None, socket.AF_INET
|
|
301
|
+
):
|
|
302
|
+
ip: str = sockaddr[0]
|
|
303
|
+
if not ip.startswith("127."):
|
|
304
|
+
packed = socket.inet_aton(ip)
|
|
305
|
+
if packed not in addrs:
|
|
306
|
+
addrs.append(packed)
|
|
307
|
+
except Exception as exc:
|
|
308
|
+
logger.debug("mDNS: address detection failed: %s", exc)
|
|
309
|
+
|
|
310
|
+
if not addrs:
|
|
311
|
+
addrs = [socket.inet_aton("127.0.0.1")]
|
|
312
|
+
|
|
313
|
+
return addrs
|