@smilintux/skcapstone 0.1.0 → 0.2.4
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 +880 -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 +191 -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 +398 -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 +357 -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 +264 -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,611 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sovereign Heartbeat v2 — active health beacon for agent meshes.
|
|
3
|
+
|
|
4
|
+
Each agent node publishes a heartbeat file containing its current
|
|
5
|
+
state, capacity, capabilities, and TTL. Syncthing distributes these
|
|
6
|
+
files across the mesh. Other agents read heartbeats to understand
|
|
7
|
+
the network's health and available resources.
|
|
8
|
+
|
|
9
|
+
Conflict-free: each node writes only its own file. Stale heartbeats
|
|
10
|
+
(past TTL) are marked as offline.
|
|
11
|
+
|
|
12
|
+
Architecture:
|
|
13
|
+
~/.skcapstone/heartbeats/
|
|
14
|
+
├── opus.json # This node's heartbeat
|
|
15
|
+
├── lumina.json # Peer heartbeat (via Syncthing)
|
|
16
|
+
├── grok.json # Peer heartbeat
|
|
17
|
+
└── ...
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
beacon = HeartbeatBeacon(home, agent_name="opus")
|
|
21
|
+
beacon.pulse() # Publish heartbeat
|
|
22
|
+
peers = beacon.discover_peers() # Find live peers
|
|
23
|
+
health = beacon.mesh_health() # Network overview
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
import platform
|
|
32
|
+
import shutil
|
|
33
|
+
from datetime import datetime, timedelta, timezone
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any, Optional
|
|
36
|
+
|
|
37
|
+
from pydantic import BaseModel, Field, field_validator
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger("skcapstone.heartbeat")
|
|
40
|
+
|
|
41
|
+
DEFAULT_TTL_SECONDS = 300 # 5 minutes
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Models
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class HeartbeatService(BaseModel):
|
|
50
|
+
"""A backend service advertised in the heartbeat."""
|
|
51
|
+
|
|
52
|
+
name: str # "skvector", "skgraph"
|
|
53
|
+
port: int # 6333, 6379
|
|
54
|
+
protocol: str = "http" # http, redis
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class AgentCapability(BaseModel):
|
|
58
|
+
"""A single agent capability."""
|
|
59
|
+
|
|
60
|
+
name: str
|
|
61
|
+
version: str = "1.0"
|
|
62
|
+
enabled: bool = True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class NodeCapacity(BaseModel):
|
|
66
|
+
"""Resource capacity of a node."""
|
|
67
|
+
|
|
68
|
+
cpu_count: int = 0
|
|
69
|
+
memory_total_mb: int = 0
|
|
70
|
+
memory_available_mb: int = 0
|
|
71
|
+
disk_free_gb: float = 0.0
|
|
72
|
+
gpu_available: bool = False
|
|
73
|
+
gpu_name: str = ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Heartbeat(BaseModel):
|
|
77
|
+
"""A single agent heartbeat beacon."""
|
|
78
|
+
|
|
79
|
+
agent_name: str
|
|
80
|
+
status: str = "alive" # alive, busy, draining, offline
|
|
81
|
+
|
|
82
|
+
@field_validator("agent_name")
|
|
83
|
+
@classmethod
|
|
84
|
+
def _validate_agent_name(cls, v: str) -> str:
|
|
85
|
+
if not v or not v.strip():
|
|
86
|
+
raise ValueError("agent_name must be a non-empty string")
|
|
87
|
+
return v.strip()
|
|
88
|
+
hostname: str = ""
|
|
89
|
+
platform: str = ""
|
|
90
|
+
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
91
|
+
ttl_seconds: int = DEFAULT_TTL_SECONDS
|
|
92
|
+
uptime_hours: float = 0.0
|
|
93
|
+
|
|
94
|
+
# Agent state
|
|
95
|
+
soul_active: str = ""
|
|
96
|
+
claimed_tasks: list[str] = Field(default_factory=list)
|
|
97
|
+
loaded_model: str = ""
|
|
98
|
+
session_active: bool = False
|
|
99
|
+
consciousness_active: bool = False
|
|
100
|
+
|
|
101
|
+
# Live metrics (new in v2.1)
|
|
102
|
+
uptime_seconds: float = 0.0
|
|
103
|
+
cpu_load_1min: float = 0.0
|
|
104
|
+
memory_used_mb: int = 0
|
|
105
|
+
active_conversations: int = 0
|
|
106
|
+
messages_processed_24h: int = 0
|
|
107
|
+
|
|
108
|
+
# Resources
|
|
109
|
+
capacity: NodeCapacity = Field(default_factory=NodeCapacity)
|
|
110
|
+
|
|
111
|
+
# Capabilities
|
|
112
|
+
capabilities: list[AgentCapability] = Field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
# Metadata
|
|
115
|
+
version: str = ""
|
|
116
|
+
fingerprint: str = ""
|
|
117
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
118
|
+
|
|
119
|
+
# Service advertisement (optional — old heartbeats without these still parse)
|
|
120
|
+
services: list[HeartbeatService] = Field(default_factory=list)
|
|
121
|
+
tailscale_ip: str = ""
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def is_alive(self) -> bool:
|
|
125
|
+
"""Whether this heartbeat is still valid (within TTL)."""
|
|
126
|
+
expires = self.timestamp + timedelta(seconds=self.ttl_seconds)
|
|
127
|
+
return datetime.now(timezone.utc) <= expires
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def age_seconds(self) -> float:
|
|
131
|
+
"""Seconds since this heartbeat was published."""
|
|
132
|
+
delta = datetime.now(timezone.utc) - self.timestamp
|
|
133
|
+
return delta.total_seconds()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class PeerInfo(BaseModel):
|
|
137
|
+
"""Summary of a discovered peer."""
|
|
138
|
+
|
|
139
|
+
agent_name: str
|
|
140
|
+
status: str
|
|
141
|
+
alive: bool
|
|
142
|
+
age_seconds: float
|
|
143
|
+
hostname: str = ""
|
|
144
|
+
capabilities: list[str] = Field(default_factory=list)
|
|
145
|
+
soul_active: str = ""
|
|
146
|
+
claimed_tasks: int = 0
|
|
147
|
+
services: list[str] = Field(default_factory=list)
|
|
148
|
+
tailscale_ip: str = ""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class MeshHealth(BaseModel):
|
|
152
|
+
"""Health summary of the agent mesh."""
|
|
153
|
+
|
|
154
|
+
total_peers: int = 0
|
|
155
|
+
alive_peers: int = 0
|
|
156
|
+
offline_peers: int = 0
|
|
157
|
+
busy_peers: int = 0
|
|
158
|
+
total_capabilities: list[str] = Field(default_factory=list)
|
|
159
|
+
peers: list[PeerInfo] = Field(default_factory=list)
|
|
160
|
+
collected_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# HeartbeatBeacon
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class HeartbeatBeacon:
|
|
169
|
+
"""Active health beacon for sovereign agent meshes.
|
|
170
|
+
|
|
171
|
+
Publishes heartbeats, discovers peers, and reports mesh health.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
home: Agent home directory (~/.skcapstone).
|
|
175
|
+
agent_name: Name of the local agent.
|
|
176
|
+
ttl_seconds: Heartbeat TTL before considered stale.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
home: Path,
|
|
182
|
+
agent_name: str = "anonymous",
|
|
183
|
+
ttl_seconds: int = DEFAULT_TTL_SECONDS,
|
|
184
|
+
heartbeats_dir: Optional[Path] = None,
|
|
185
|
+
) -> None:
|
|
186
|
+
if not agent_name or not agent_name.strip():
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"agent_name must be a non-empty string, got %r" % agent_name
|
|
189
|
+
)
|
|
190
|
+
self._home = home
|
|
191
|
+
self._agent = agent_name.strip()
|
|
192
|
+
self._ttl = ttl_seconds
|
|
193
|
+
self._heartbeat_dir = Path(heartbeats_dir) if heartbeats_dir else home / "heartbeats"
|
|
194
|
+
self._start_time = datetime.now(timezone.utc)
|
|
195
|
+
|
|
196
|
+
def initialize(self) -> None:
|
|
197
|
+
"""Create the heartbeat directory."""
|
|
198
|
+
self._heartbeat_dir.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
|
|
200
|
+
def pulse(
|
|
201
|
+
self,
|
|
202
|
+
status: str = "alive",
|
|
203
|
+
claimed_tasks: Optional[list[str]] = None,
|
|
204
|
+
loaded_model: str = "",
|
|
205
|
+
capabilities: Optional[list[AgentCapability]] = None,
|
|
206
|
+
metadata: Optional[dict[str, Any]] = None,
|
|
207
|
+
services: Optional[list[HeartbeatService]] = None,
|
|
208
|
+
tailscale_ip: Optional[str] = None,
|
|
209
|
+
consciousness_active: bool = False,
|
|
210
|
+
active_conversations: int = 0,
|
|
211
|
+
messages_processed_24h: int = 0,
|
|
212
|
+
) -> Heartbeat:
|
|
213
|
+
"""Publish a heartbeat beacon.
|
|
214
|
+
|
|
215
|
+
Writes the agent's current state to its heartbeat file.
|
|
216
|
+
Only writes to its own file — never touches peer files.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
status: Agent status (alive, busy, draining, offline).
|
|
220
|
+
claimed_tasks: Currently claimed task IDs.
|
|
221
|
+
loaded_model: Currently loaded AI model.
|
|
222
|
+
capabilities: Agent capabilities list.
|
|
223
|
+
metadata: Additional metadata.
|
|
224
|
+
services: Backend services to advertise. Auto-detected if None.
|
|
225
|
+
tailscale_ip: Tailscale IP address. Auto-detected if None.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
The published Heartbeat.
|
|
229
|
+
"""
|
|
230
|
+
self.initialize()
|
|
231
|
+
|
|
232
|
+
uptime_secs = (datetime.now(timezone.utc) - self._start_time).total_seconds()
|
|
233
|
+
uptime_hours = uptime_secs / 3600
|
|
234
|
+
cpu_load, mem_used_mb = self._detect_cpu_and_mem()
|
|
235
|
+
|
|
236
|
+
heartbeat = Heartbeat(
|
|
237
|
+
agent_name=self._agent,
|
|
238
|
+
status=status,
|
|
239
|
+
hostname=platform.node(),
|
|
240
|
+
platform=f"{platform.system()} {platform.machine()}",
|
|
241
|
+
ttl_seconds=self._ttl,
|
|
242
|
+
uptime_hours=round(uptime_hours, 2),
|
|
243
|
+
claimed_tasks=claimed_tasks or [],
|
|
244
|
+
loaded_model=loaded_model,
|
|
245
|
+
session_active=True,
|
|
246
|
+
consciousness_active=consciousness_active,
|
|
247
|
+
uptime_seconds=round(uptime_secs, 1),
|
|
248
|
+
cpu_load_1min=cpu_load,
|
|
249
|
+
memory_used_mb=mem_used_mb,
|
|
250
|
+
active_conversations=active_conversations,
|
|
251
|
+
messages_processed_24h=messages_processed_24h,
|
|
252
|
+
capacity=self._detect_capacity(),
|
|
253
|
+
capabilities=capabilities or self._detect_capabilities(),
|
|
254
|
+
version=self._detect_version(),
|
|
255
|
+
fingerprint=self._detect_fingerprint(),
|
|
256
|
+
soul_active=self._detect_soul(),
|
|
257
|
+
metadata=metadata or {},
|
|
258
|
+
services=services if services is not None else self._detect_services(),
|
|
259
|
+
tailscale_ip=tailscale_ip if tailscale_ip is not None else self._detect_tailscale_ip(),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
path = self._heartbeat_dir / f"{self._agent.lower()}.json"
|
|
263
|
+
tmp_path = path.with_suffix(".json.tmp")
|
|
264
|
+
tmp_path.write_text(
|
|
265
|
+
heartbeat.model_dump_json(indent=2),
|
|
266
|
+
encoding="utf-8",
|
|
267
|
+
)
|
|
268
|
+
tmp_path.rename(path)
|
|
269
|
+
|
|
270
|
+
logger.debug("Heartbeat pulse: %s status=%s", self._agent, status)
|
|
271
|
+
return heartbeat
|
|
272
|
+
|
|
273
|
+
def read_heartbeat(self, agent_name: str) -> Optional[Heartbeat]:
|
|
274
|
+
"""Read a specific agent's heartbeat.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
agent_name: The agent to read.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Heartbeat or None if not found.
|
|
281
|
+
"""
|
|
282
|
+
path = self._heartbeat_dir / f"{agent_name}.json"
|
|
283
|
+
if not path.exists():
|
|
284
|
+
return None
|
|
285
|
+
try:
|
|
286
|
+
return Heartbeat.model_validate_json(
|
|
287
|
+
path.read_text(encoding="utf-8")
|
|
288
|
+
)
|
|
289
|
+
except Exception as exc:
|
|
290
|
+
logger.warning("Cannot read heartbeat for %s: %s", agent_name, exc)
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
def discover_peers(self, include_self: bool = False) -> list[PeerInfo]:
|
|
294
|
+
"""Discover all peers from heartbeat files.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
include_self: Whether to include own heartbeat.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
List of PeerInfo for all discovered agents.
|
|
301
|
+
"""
|
|
302
|
+
self.initialize()
|
|
303
|
+
peers: list[PeerInfo] = []
|
|
304
|
+
|
|
305
|
+
for f in sorted(self._heartbeat_dir.glob("*.json")):
|
|
306
|
+
if f.name.endswith(".tmp"):
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
agent_name = f.stem
|
|
310
|
+
if not include_self and agent_name == self._agent:
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
hb = Heartbeat.model_validate_json(
|
|
315
|
+
f.read_text(encoding="utf-8")
|
|
316
|
+
)
|
|
317
|
+
peers.append(PeerInfo(
|
|
318
|
+
agent_name=hb.agent_name,
|
|
319
|
+
status=hb.status if hb.is_alive else "offline",
|
|
320
|
+
alive=hb.is_alive,
|
|
321
|
+
age_seconds=round(hb.age_seconds, 1),
|
|
322
|
+
hostname=hb.hostname,
|
|
323
|
+
capabilities=[c.name for c in hb.capabilities if c.enabled],
|
|
324
|
+
soul_active=hb.soul_active,
|
|
325
|
+
claimed_tasks=len(hb.claimed_tasks),
|
|
326
|
+
services=[s.name for s in hb.services],
|
|
327
|
+
tailscale_ip=hb.tailscale_ip,
|
|
328
|
+
))
|
|
329
|
+
except Exception as exc:
|
|
330
|
+
logger.warning("Cannot parse heartbeat %s: %s", f.name, exc)
|
|
331
|
+
|
|
332
|
+
return peers
|
|
333
|
+
|
|
334
|
+
def mesh_health(self) -> MeshHealth:
|
|
335
|
+
"""Get overall mesh health summary.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
MeshHealth with peer counts and capability overview.
|
|
339
|
+
"""
|
|
340
|
+
peers = self.discover_peers(include_self=True)
|
|
341
|
+
alive = [p for p in peers if p.alive]
|
|
342
|
+
offline = [p for p in peers if not p.alive]
|
|
343
|
+
busy = [p for p in peers if p.status == "busy"]
|
|
344
|
+
|
|
345
|
+
all_caps: set[str] = set()
|
|
346
|
+
for p in alive:
|
|
347
|
+
all_caps.update(p.capabilities)
|
|
348
|
+
|
|
349
|
+
return MeshHealth(
|
|
350
|
+
total_peers=len(peers),
|
|
351
|
+
alive_peers=len(alive),
|
|
352
|
+
offline_peers=len(offline),
|
|
353
|
+
busy_peers=len(busy),
|
|
354
|
+
total_capabilities=sorted(all_caps),
|
|
355
|
+
peers=peers,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
def find_capable(self, capability: str) -> list[PeerInfo]:
|
|
359
|
+
"""Find alive peers with a specific capability.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
capability: The capability name to search for.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
List of alive peers with the capability.
|
|
366
|
+
"""
|
|
367
|
+
peers = self.discover_peers(include_self=True)
|
|
368
|
+
return [
|
|
369
|
+
p for p in peers
|
|
370
|
+
if p.alive and capability in p.capabilities
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
def mark_offline(self) -> None:
|
|
374
|
+
"""Mark this agent as going offline.
|
|
375
|
+
|
|
376
|
+
Publishes a final heartbeat with status "offline" and
|
|
377
|
+
TTL of 0 so peers know immediately.
|
|
378
|
+
"""
|
|
379
|
+
self.pulse(status="offline")
|
|
380
|
+
|
|
381
|
+
# -------------------------------------------------------------------
|
|
382
|
+
# Internal helpers
|
|
383
|
+
# -------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
def _detect_cpu_and_mem(self) -> tuple[float, int]:
|
|
386
|
+
"""Return (cpu_load_1min, memory_used_mb) using psutil or /proc fallback.
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
Tuple of (1-minute CPU load average, memory used in MB).
|
|
390
|
+
"""
|
|
391
|
+
cpu_load = 0.0
|
|
392
|
+
mem_used_mb = 0
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
import psutil
|
|
396
|
+
loads = psutil.getloadavg()
|
|
397
|
+
cpu_load = round(loads[0], 2)
|
|
398
|
+
mem = psutil.virtual_memory()
|
|
399
|
+
mem_used_mb = (mem.total - mem.available) // (1024 * 1024)
|
|
400
|
+
except ImportError:
|
|
401
|
+
# Fallback: /proc on Linux
|
|
402
|
+
try:
|
|
403
|
+
loadavg = Path("/proc/loadavg")
|
|
404
|
+
if loadavg.exists():
|
|
405
|
+
parts = loadavg.read_text().split()
|
|
406
|
+
cpu_load = round(float(parts[0]), 2)
|
|
407
|
+
except Exception as exc:
|
|
408
|
+
logger.debug("CPU load fallback failed: %s", exc)
|
|
409
|
+
try:
|
|
410
|
+
meminfo = Path("/proc/meminfo")
|
|
411
|
+
if meminfo.exists():
|
|
412
|
+
info: dict[str, int] = {}
|
|
413
|
+
for line in meminfo.read_text().splitlines():
|
|
414
|
+
parts = line.split()
|
|
415
|
+
if len(parts) >= 2:
|
|
416
|
+
info[parts[0].rstrip(":")] = int(parts[1])
|
|
417
|
+
total_kb = info.get("MemTotal", 0)
|
|
418
|
+
avail_kb = info.get("MemAvailable", 0)
|
|
419
|
+
mem_used_mb = (total_kb - avail_kb) // 1024
|
|
420
|
+
except Exception as exc:
|
|
421
|
+
logger.debug("Memory fallback failed: %s", exc)
|
|
422
|
+
except Exception as exc:
|
|
423
|
+
logger.debug("CPU/mem detection failed: %s", exc)
|
|
424
|
+
|
|
425
|
+
return cpu_load, mem_used_mb
|
|
426
|
+
|
|
427
|
+
def _detect_capacity(self) -> NodeCapacity:
|
|
428
|
+
"""Detect current node resource capacity."""
|
|
429
|
+
try:
|
|
430
|
+
cpu_count = os.cpu_count() or 0
|
|
431
|
+
disk = shutil.disk_usage(self._home)
|
|
432
|
+
disk_free_gb = round(disk.free / (1024 ** 3), 1)
|
|
433
|
+
|
|
434
|
+
mem_total = 0
|
|
435
|
+
mem_avail = 0
|
|
436
|
+
try:
|
|
437
|
+
import psutil
|
|
438
|
+
mem = psutil.virtual_memory()
|
|
439
|
+
mem_total = mem.total // (1024 * 1024)
|
|
440
|
+
mem_avail = mem.available // (1024 * 1024)
|
|
441
|
+
except ImportError:
|
|
442
|
+
# Fallback: read from /proc/meminfo on Linux
|
|
443
|
+
meminfo = Path("/proc/meminfo")
|
|
444
|
+
if meminfo.exists():
|
|
445
|
+
for line in meminfo.read_text().splitlines():
|
|
446
|
+
if line.startswith("MemTotal:"):
|
|
447
|
+
mem_total = int(line.split()[1]) // 1024
|
|
448
|
+
elif line.startswith("MemAvailable:"):
|
|
449
|
+
mem_avail = int(line.split()[1]) // 1024
|
|
450
|
+
|
|
451
|
+
gpu_available = False
|
|
452
|
+
gpu_name = ""
|
|
453
|
+
if shutil.which("nvidia-smi"):
|
|
454
|
+
gpu_available = True
|
|
455
|
+
gpu_name = "nvidia"
|
|
456
|
+
|
|
457
|
+
return NodeCapacity(
|
|
458
|
+
cpu_count=cpu_count,
|
|
459
|
+
memory_total_mb=mem_total,
|
|
460
|
+
memory_available_mb=mem_avail,
|
|
461
|
+
disk_free_gb=disk_free_gb,
|
|
462
|
+
gpu_available=gpu_available,
|
|
463
|
+
gpu_name=gpu_name,
|
|
464
|
+
)
|
|
465
|
+
except Exception as exc:
|
|
466
|
+
logger.warning("Failed to detect node capacity: %s", exc)
|
|
467
|
+
return NodeCapacity()
|
|
468
|
+
|
|
469
|
+
def _load_config_capabilities(self) -> list[str]:
|
|
470
|
+
"""Load capability names from the agent config file.
|
|
471
|
+
|
|
472
|
+
Reads ``{home}/config/config.yaml`` and returns the ``capabilities``
|
|
473
|
+
list. Falls back to the ``AgentConfig`` defaults if the file is
|
|
474
|
+
absent or unparseable.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
List of capability name strings from config.
|
|
478
|
+
"""
|
|
479
|
+
config_path = self._home / "config" / "config.yaml"
|
|
480
|
+
try:
|
|
481
|
+
if config_path.exists():
|
|
482
|
+
import yaml as _yaml
|
|
483
|
+
data = _yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
|
484
|
+
caps = data.get("capabilities")
|
|
485
|
+
if isinstance(caps, list):
|
|
486
|
+
return [str(c) for c in caps if c]
|
|
487
|
+
except Exception as exc:
|
|
488
|
+
logger.debug("Cannot load capabilities from config: %s", exc)
|
|
489
|
+
# Fall back to AgentConfig defaults
|
|
490
|
+
from .models import AgentConfig
|
|
491
|
+
return AgentConfig().capabilities
|
|
492
|
+
|
|
493
|
+
def _detect_capabilities(self) -> list[AgentCapability]:
|
|
494
|
+
"""Build capabilities list from config, merging with detected packages.
|
|
495
|
+
|
|
496
|
+
Config capabilities (from ``{home}/config/config.yaml``) are always
|
|
497
|
+
included. Additionally, known optional packages are probed via
|
|
498
|
+
``import``; any that are present but not already listed by config are
|
|
499
|
+
appended.
|
|
500
|
+
"""
|
|
501
|
+
config_caps = self._load_config_capabilities()
|
|
502
|
+
cap_names: list[str] = list(config_caps)
|
|
503
|
+
|
|
504
|
+
# Probe optional packages and add if not already declared in config
|
|
505
|
+
package_checks = [
|
|
506
|
+
("skcapstone", "skcapstone"),
|
|
507
|
+
("skmemory", "skmemory"),
|
|
508
|
+
("skchat", "skchat"),
|
|
509
|
+
("skcomm", "skcomm"),
|
|
510
|
+
("capauth", "capauth"),
|
|
511
|
+
("cloud9", "cloud9"),
|
|
512
|
+
]
|
|
513
|
+
for name, module in package_checks:
|
|
514
|
+
if name not in cap_names:
|
|
515
|
+
try:
|
|
516
|
+
__import__(module)
|
|
517
|
+
cap_names.append(name)
|
|
518
|
+
except ImportError:
|
|
519
|
+
pass
|
|
520
|
+
|
|
521
|
+
return [AgentCapability(name=n) for n in cap_names]
|
|
522
|
+
|
|
523
|
+
def _detect_version(self) -> str:
|
|
524
|
+
"""Detect skcapstone version."""
|
|
525
|
+
try:
|
|
526
|
+
from . import __version__
|
|
527
|
+
return __version__
|
|
528
|
+
except Exception as exc:
|
|
529
|
+
logger.debug("Version detection failed: %s", exc)
|
|
530
|
+
return "unknown"
|
|
531
|
+
|
|
532
|
+
def _detect_fingerprint(self) -> str:
|
|
533
|
+
"""Detect agent identity fingerprint."""
|
|
534
|
+
identity_path = self._home / "identity" / "identity.json"
|
|
535
|
+
if identity_path.exists():
|
|
536
|
+
try:
|
|
537
|
+
data = json.loads(identity_path.read_text(encoding="utf-8"))
|
|
538
|
+
return data.get("fingerprint", "")[:16]
|
|
539
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
540
|
+
logger.debug("Cannot read identity for fingerprint: %s", exc)
|
|
541
|
+
return ""
|
|
542
|
+
|
|
543
|
+
def _detect_soul(self) -> str:
|
|
544
|
+
"""Detect active soul overlay."""
|
|
545
|
+
active_path = self._home / "soul" / "active.json"
|
|
546
|
+
if active_path.exists():
|
|
547
|
+
try:
|
|
548
|
+
data = json.loads(active_path.read_text(encoding="utf-8"))
|
|
549
|
+
return data.get("active_soul", "")
|
|
550
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
551
|
+
logger.debug("Cannot read soul overlay: %s", exc)
|
|
552
|
+
return ""
|
|
553
|
+
|
|
554
|
+
def _detect_services(self) -> list[HeartbeatService]:
|
|
555
|
+
"""Auto-detect locally running backend services.
|
|
556
|
+
|
|
557
|
+
Checks if well-known ports are listening on localhost.
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
List of detected HeartbeatService entries.
|
|
561
|
+
"""
|
|
562
|
+
import socket as _socket
|
|
563
|
+
|
|
564
|
+
services: list[HeartbeatService] = []
|
|
565
|
+
checks = [
|
|
566
|
+
("skvector", 6333, "http"),
|
|
567
|
+
("skgraph", 6379, "redis"),
|
|
568
|
+
]
|
|
569
|
+
|
|
570
|
+
for name, port, protocol in checks:
|
|
571
|
+
try:
|
|
572
|
+
s = _socket.create_connection(("127.0.0.1", port), timeout=1)
|
|
573
|
+
s.close()
|
|
574
|
+
services.append(HeartbeatService(
|
|
575
|
+
name=name, port=port, protocol=protocol,
|
|
576
|
+
))
|
|
577
|
+
except (OSError, _socket.timeout):
|
|
578
|
+
pass
|
|
579
|
+
|
|
580
|
+
return services
|
|
581
|
+
|
|
582
|
+
def _detect_tailscale_ip(self) -> str:
|
|
583
|
+
"""Best-effort Tailscale IP detection.
|
|
584
|
+
|
|
585
|
+
Runs ``tailscale status --json`` and extracts the self IP.
|
|
586
|
+
Fails silently if Tailscale is not installed or not running.
|
|
587
|
+
|
|
588
|
+
Returns:
|
|
589
|
+
Tailscale IPv4 address or empty string.
|
|
590
|
+
"""
|
|
591
|
+
import subprocess
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
result = subprocess.run(
|
|
595
|
+
["tailscale", "status", "--json"],
|
|
596
|
+
capture_output=True,
|
|
597
|
+
text=True,
|
|
598
|
+
timeout=3,
|
|
599
|
+
)
|
|
600
|
+
if result.returncode == 0:
|
|
601
|
+
data = json.loads(result.stdout)
|
|
602
|
+
ts_ips = data.get("Self", {}).get("TailscaleIPs", [])
|
|
603
|
+
# Prefer IPv4
|
|
604
|
+
for ip in ts_ips:
|
|
605
|
+
if "." in ip:
|
|
606
|
+
return ip
|
|
607
|
+
if ts_ips:
|
|
608
|
+
return ts_ips[0]
|
|
609
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError, OSError):
|
|
610
|
+
pass
|
|
611
|
+
return ""
|