@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,759 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Docker Provider — deploy agent teams as Docker containers.
|
|
3
|
+
|
|
4
|
+
Each agent runs in its own container with resource limits derived from
|
|
5
|
+
the blueprint ResourceSpec. Supports both individual container management
|
|
6
|
+
and docker-compose generation for full team orchestration.
|
|
7
|
+
|
|
8
|
+
The provider wires three sovereign infrastructure components into every
|
|
9
|
+
agent container:
|
|
10
|
+
|
|
11
|
+
1. **Soul Blueprint** — injected via SOUL_BLUEPRINT env and config.json
|
|
12
|
+
2. **MCP Server** — host-side skcapstone MCP reachable via env
|
|
13
|
+
(SKCAPSTONE_MCP_HOST / SKCAPSTONE_MCP_SOCKET). Set one of these so
|
|
14
|
+
containers can call memory_store, coord_claim, etc.
|
|
15
|
+
3. **SKComm Transport** — comms directory bind-mounted at /skcomm so
|
|
16
|
+
containers share the same file-channel inboxes as local agents.
|
|
17
|
+
|
|
18
|
+
Prerequisites:
|
|
19
|
+
- Docker daemon running and accessible (DOCKER_HOST or default socket)
|
|
20
|
+
- docker Python SDK: pip install docker
|
|
21
|
+
- Optional: DOCKER_BASE_IMAGE env var to override the default image
|
|
22
|
+
- Optional: SKCOMM_HOME env var for the comms directory
|
|
23
|
+
- Optional: SKCAPSTONE_MCP_HOST env var (host:port) or
|
|
24
|
+
SKCAPSTONE_MCP_SOCKET env var (unix socket path)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any, Dict, List, Optional
|
|
34
|
+
|
|
35
|
+
import yaml
|
|
36
|
+
|
|
37
|
+
from ..blueprints.schema import AgentSpec, BlueprintManifest, ProviderType
|
|
38
|
+
from ..team_engine import AgentStatus, ProviderBackend
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
_DEFAULT_IMAGE = "python:3.12-slim"
|
|
43
|
+
_GRACEFUL_STOP_TIMEOUT = 15 # seconds before SIGKILL
|
|
44
|
+
_MCP_CONTAINER_SOCKET = "/run/skcapstone/mcp.sock" # path inside containers
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _parse_memory_bytes(mem_str: str) -> int:
|
|
48
|
+
"""Convert memory string like '2g' or '512m' to bytes.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
mem_str: Memory string with unit suffix (g/G for gigabytes, m/M for
|
|
52
|
+
megabytes).
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Memory in bytes as an integer.
|
|
56
|
+
"""
|
|
57
|
+
mem_str = mem_str.strip().lower()
|
|
58
|
+
if mem_str.endswith("g"):
|
|
59
|
+
return int(float(mem_str[:-1]) * 1024 * 1024 * 1024)
|
|
60
|
+
if mem_str.endswith("m"):
|
|
61
|
+
return int(float(mem_str[:-1]) * 1024 * 1024)
|
|
62
|
+
return int(mem_str)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _nano_cpus(cores: int) -> int:
|
|
66
|
+
"""Convert CPU core count to Docker nano_cpus value.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
cores: Number of CPU cores.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
NanoCPUs value (cores * 1e9).
|
|
73
|
+
"""
|
|
74
|
+
return cores * 1_000_000_000
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class DockerProvider(ProviderBackend):
|
|
78
|
+
"""Deploy agent teams as Docker containers.
|
|
79
|
+
|
|
80
|
+
Each agent spec maps to one (or more) containers with resource limits,
|
|
81
|
+
environment variables, and a mounted config volume. The provider also
|
|
82
|
+
supports generating a docker-compose.yml for full team orchestration.
|
|
83
|
+
|
|
84
|
+
Sovereign infrastructure wiring
|
|
85
|
+
--------------------------------
|
|
86
|
+
- **SKComm**: pass ``skcomm_home`` (or set SKCOMM_HOME) to bind-mount
|
|
87
|
+
the comms directory at ``/skcomm`` inside every container so
|
|
88
|
+
container agents share the same file-channel inboxes.
|
|
89
|
+
- **MCP server**: pass ``mcp_host`` (host:port) or ``mcp_socket_path``
|
|
90
|
+
to inject the skcapstone MCP endpoint into container env. Containers
|
|
91
|
+
can then call skcapstone memory, coordination, and heartbeat tools.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
base_image: Default Docker image for agent containers.
|
|
95
|
+
network_name: Docker network to attach containers to.
|
|
96
|
+
volume_prefix: Prefix for named volumes created per agent.
|
|
97
|
+
docker_host: Docker daemon socket/URL (default: DOCKER_HOST or
|
|
98
|
+
``unix:///var/run/docker.sock``).
|
|
99
|
+
skcomm_home: Host-side SKComm comms root directory; bind-mounted at
|
|
100
|
+
``/skcomm`` inside containers. Reads SKCOMM_HOME if not set.
|
|
101
|
+
mcp_host: Host:port of the skcapstone MCP server (e.g.
|
|
102
|
+
``"host-gateway:8765"``). Sets SKCAPSTONE_MCP_HOST inside
|
|
103
|
+
containers. Reads SKCAPSTONE_MCP_HOST env if not set.
|
|
104
|
+
mcp_socket_path: Host-side Unix socket for the MCP server. Bind-
|
|
105
|
+
mounted at /run/skcapstone/mcp.sock and sets
|
|
106
|
+
SKCAPSTONE_MCP_SOCKET inside containers. Reads
|
|
107
|
+
SKCAPSTONE_MCP_SOCKET env if not set.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
provider_type = ProviderType.DOCKER
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
base_image: Optional[str] = None,
|
|
115
|
+
network_name: str = "skcapstone",
|
|
116
|
+
volume_prefix: str = "skcapstone-agent",
|
|
117
|
+
docker_host: Optional[str] = None,
|
|
118
|
+
skcomm_home: Optional[str] = None,
|
|
119
|
+
mcp_host: Optional[str] = None,
|
|
120
|
+
mcp_socket_path: Optional[str] = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
self._base_image = (
|
|
123
|
+
base_image
|
|
124
|
+
or os.environ.get("DOCKER_BASE_IMAGE", _DEFAULT_IMAGE)
|
|
125
|
+
)
|
|
126
|
+
self._network_name = network_name
|
|
127
|
+
self._volume_prefix = volume_prefix
|
|
128
|
+
self._docker_host = docker_host or os.environ.get("DOCKER_HOST", "")
|
|
129
|
+
self._skcomm_home = skcomm_home or os.environ.get("SKCOMM_HOME", "")
|
|
130
|
+
self._mcp_host = mcp_host or os.environ.get("SKCAPSTONE_MCP_HOST", "")
|
|
131
|
+
self._mcp_socket_path = (
|
|
132
|
+
mcp_socket_path or os.environ.get("SKCAPSTONE_MCP_SOCKET", "")
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
# Internal helpers
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def _client(self):
|
|
140
|
+
"""Return an authenticated Docker client.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
docker.DockerClient instance.
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
RuntimeError: If the docker SDK is not installed or the daemon
|
|
147
|
+
is unreachable.
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
import docker
|
|
151
|
+
except ImportError:
|
|
152
|
+
raise RuntimeError(
|
|
153
|
+
"Docker provider requires 'docker' SDK: pip install docker"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
kwargs: Dict[str, Any] = {}
|
|
157
|
+
if self._docker_host:
|
|
158
|
+
kwargs["base_url"] = self._docker_host
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
client = docker.from_env(**kwargs)
|
|
162
|
+
client.ping()
|
|
163
|
+
return client
|
|
164
|
+
except Exception as exc:
|
|
165
|
+
raise RuntimeError(
|
|
166
|
+
f"Cannot connect to Docker daemon: {exc}"
|
|
167
|
+
) from exc
|
|
168
|
+
|
|
169
|
+
def _ensure_network(self, client) -> None:
|
|
170
|
+
"""Create the shared Docker network if it does not exist.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
client: docker.DockerClient instance.
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
client.networks.get(self._network_name)
|
|
177
|
+
except Exception:
|
|
178
|
+
client.networks.create(
|
|
179
|
+
self._network_name,
|
|
180
|
+
driver="bridge",
|
|
181
|
+
check_duplicate=True,
|
|
182
|
+
)
|
|
183
|
+
logger.info("Created Docker network: %s", self._network_name)
|
|
184
|
+
|
|
185
|
+
def _volume_name(self, agent_name: str) -> str:
|
|
186
|
+
"""Derive the named volume for an agent.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
agent_name: Agent instance name.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Docker volume name string.
|
|
193
|
+
"""
|
|
194
|
+
safe = agent_name.replace("_", "-").lower()
|
|
195
|
+
return f"{self._volume_prefix}-{safe}"
|
|
196
|
+
|
|
197
|
+
def _container_name(self, agent_name: str) -> str:
|
|
198
|
+
"""Derive the container name for an agent.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
agent_name: Agent instance name.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Docker container name string.
|
|
205
|
+
"""
|
|
206
|
+
return agent_name.replace("_", "-").lower()
|
|
207
|
+
|
|
208
|
+
def _build_agent_config(self, agent_name: str, spec: AgentSpec, team_name: str) -> Dict[str, Any]:
|
|
209
|
+
"""Build the agent config dict written into the container.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
agent_name: Agent instance name.
|
|
213
|
+
spec: Agent specification.
|
|
214
|
+
team_name: Parent team name.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Config dict ready for JSON serialisation.
|
|
218
|
+
"""
|
|
219
|
+
return {
|
|
220
|
+
"agent_name": agent_name,
|
|
221
|
+
"team_name": team_name,
|
|
222
|
+
"role": spec.role.value,
|
|
223
|
+
"model": spec.model_name or spec.model.value,
|
|
224
|
+
"skills": spec.skills,
|
|
225
|
+
"soul_blueprint": spec.soul_blueprint,
|
|
226
|
+
"env": spec.env,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
def _build_sovereign_env(self, env_vars: Dict[str, str]) -> None:
|
|
230
|
+
"""Inject sovereign infrastructure env vars into the env dict in-place.
|
|
231
|
+
|
|
232
|
+
Adds MCP server endpoint and SKComm home when configured so
|
|
233
|
+
container agents can reach the host-side sovereign stack.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
env_vars: Environment variable dict to mutate.
|
|
237
|
+
"""
|
|
238
|
+
if self._mcp_host:
|
|
239
|
+
env_vars["SKCAPSTONE_MCP_HOST"] = self._mcp_host
|
|
240
|
+
if self._mcp_socket_path:
|
|
241
|
+
env_vars["SKCAPSTONE_MCP_SOCKET"] = _MCP_CONTAINER_SOCKET
|
|
242
|
+
if self._skcomm_home:
|
|
243
|
+
env_vars["SKCOMM_HOME"] = "/skcomm"
|
|
244
|
+
|
|
245
|
+
def _build_volumes_config(self, volume_name: str) -> Dict[str, Any]:
|
|
246
|
+
"""Build the volumes dict for containers.create().
|
|
247
|
+
|
|
248
|
+
Always includes the per-agent named volume at /agent.
|
|
249
|
+
Optionally adds a bind mount for the SKComm comms directory.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
volume_name: Named Docker volume for agent state.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Volumes dict suitable for docker SDK containers.create().
|
|
256
|
+
"""
|
|
257
|
+
vols: Dict[str, Any] = {
|
|
258
|
+
volume_name: {"bind": "/agent", "mode": "rw"},
|
|
259
|
+
}
|
|
260
|
+
if self._skcomm_home and Path(self._skcomm_home).exists():
|
|
261
|
+
vols[self._skcomm_home] = {"bind": "/skcomm", "mode": "rw"}
|
|
262
|
+
if self._mcp_socket_path and Path(self._mcp_socket_path).exists():
|
|
263
|
+
vols[self._mcp_socket_path] = {
|
|
264
|
+
"bind": _MCP_CONTAINER_SOCKET,
|
|
265
|
+
"mode": "ro",
|
|
266
|
+
}
|
|
267
|
+
return vols
|
|
268
|
+
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
# ProviderBackend interface
|
|
271
|
+
# ------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def provision(
|
|
274
|
+
self,
|
|
275
|
+
agent_name: str,
|
|
276
|
+
spec: AgentSpec,
|
|
277
|
+
team_name: str,
|
|
278
|
+
) -> Dict[str, Any]:
|
|
279
|
+
"""Create a Docker container for one agent instance.
|
|
280
|
+
|
|
281
|
+
The container is created but NOT started here; start() does that.
|
|
282
|
+
Resource limits (CPU, memory) are applied from spec.resources.
|
|
283
|
+
SKComm and MCP sovereign infrastructure are wired in when
|
|
284
|
+
configured on this provider.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
agent_name: Unique agent instance name.
|
|
288
|
+
spec: Agent specification including resource requirements.
|
|
289
|
+
team_name: Parent team name.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Dict with 'container_id', 'container_name', 'host',
|
|
293
|
+
'volume_name', and 'team_name'.
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
RuntimeError: If Docker daemon is unreachable or container
|
|
297
|
+
creation fails.
|
|
298
|
+
"""
|
|
299
|
+
client = self._client()
|
|
300
|
+
self._ensure_network(client)
|
|
301
|
+
|
|
302
|
+
container_name = self._container_name(agent_name)
|
|
303
|
+
volume_name = self._volume_name(agent_name)
|
|
304
|
+
|
|
305
|
+
# Remove any stale container with the same name
|
|
306
|
+
try:
|
|
307
|
+
old = client.containers.get(container_name)
|
|
308
|
+
logger.warning("Removing stale container: %s", container_name)
|
|
309
|
+
old.remove(force=True)
|
|
310
|
+
except Exception:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
# Ensure named volume for agent state persistence
|
|
314
|
+
try:
|
|
315
|
+
client.volumes.get(volume_name)
|
|
316
|
+
except Exception:
|
|
317
|
+
client.volumes.create(volume_name)
|
|
318
|
+
logger.debug("Created volume: %s", volume_name)
|
|
319
|
+
|
|
320
|
+
mem_bytes = _parse_memory_bytes(spec.resources.memory)
|
|
321
|
+
nano_cpus = _nano_cpus(spec.resources.cores)
|
|
322
|
+
|
|
323
|
+
env_vars: Dict[str, str] = {
|
|
324
|
+
"AGENT_NAME": agent_name,
|
|
325
|
+
"TEAM_NAME": team_name,
|
|
326
|
+
"AGENT_ROLE": spec.role.value,
|
|
327
|
+
"AGENT_MODEL": spec.model_name or spec.model.value,
|
|
328
|
+
}
|
|
329
|
+
if spec.soul_blueprint:
|
|
330
|
+
env_vars["SOUL_BLUEPRINT"] = spec.soul_blueprint
|
|
331
|
+
env_vars.update(spec.env)
|
|
332
|
+
|
|
333
|
+
# Wire sovereign infrastructure (MCP + SKComm)
|
|
334
|
+
self._build_sovereign_env(env_vars)
|
|
335
|
+
|
|
336
|
+
volumes_config = self._build_volumes_config(volume_name)
|
|
337
|
+
|
|
338
|
+
logger.info(
|
|
339
|
+
"Creating container %s (%s, %s RAM, %d cores)",
|
|
340
|
+
container_name,
|
|
341
|
+
self._base_image,
|
|
342
|
+
spec.resources.memory,
|
|
343
|
+
spec.resources.cores,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
container = client.containers.create(
|
|
347
|
+
image=self._base_image,
|
|
348
|
+
name=container_name,
|
|
349
|
+
environment=env_vars,
|
|
350
|
+
volumes=volumes_config,
|
|
351
|
+
network=self._network_name,
|
|
352
|
+
mem_limit=mem_bytes,
|
|
353
|
+
nano_cpus=nano_cpus,
|
|
354
|
+
# Reason: Keep STDIN open so the container does not exit
|
|
355
|
+
# immediately when used with interactive agent runtimes.
|
|
356
|
+
stdin_open=True,
|
|
357
|
+
tty=False,
|
|
358
|
+
labels={
|
|
359
|
+
"managed_by": "skcapstone",
|
|
360
|
+
"team": team_name,
|
|
361
|
+
"agent": agent_name,
|
|
362
|
+
"role": spec.role.value,
|
|
363
|
+
},
|
|
364
|
+
# Restart unless explicitly stopped (resilience for long-lived agents)
|
|
365
|
+
restart_policy={"Name": "unless-stopped"},
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
"container_id": container.id,
|
|
370
|
+
"container_name": container_name,
|
|
371
|
+
"host": container_name,
|
|
372
|
+
"volume_name": volume_name,
|
|
373
|
+
"team_name": team_name, # stored so configure() can use it
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
def configure(
|
|
377
|
+
self,
|
|
378
|
+
agent_name: str,
|
|
379
|
+
spec: AgentSpec,
|
|
380
|
+
provision_result: Dict[str, Any],
|
|
381
|
+
) -> bool:
|
|
382
|
+
"""Write agent configuration into the container volume.
|
|
383
|
+
|
|
384
|
+
Injects config.json (soul blueprint, skills, model) into the
|
|
385
|
+
/agent directory via docker exec + shell printf.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
agent_name: Agent instance name.
|
|
389
|
+
spec: Agent specification.
|
|
390
|
+
provision_result: Output from provision().
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
True if configuration was written successfully.
|
|
394
|
+
"""
|
|
395
|
+
container_name = provision_result.get("container_name", "")
|
|
396
|
+
if not container_name:
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
client = self._client()
|
|
400
|
+
try:
|
|
401
|
+
container = client.containers.get(container_name)
|
|
402
|
+
except Exception as exc:
|
|
403
|
+
logger.error("Container %s not found: %s", container_name, exc)
|
|
404
|
+
return False
|
|
405
|
+
|
|
406
|
+
# Start container temporarily to write config if not already running
|
|
407
|
+
if container.status != "running":
|
|
408
|
+
container.start()
|
|
409
|
+
|
|
410
|
+
team_name = provision_result.get("team_name", "")
|
|
411
|
+
config = self._build_agent_config(agent_name, spec, team_name)
|
|
412
|
+
config_json = json.dumps(config, indent=2)
|
|
413
|
+
|
|
414
|
+
# Reason: Use exec to write the config file inside the container so
|
|
415
|
+
# no bind-mounted host path is required; the named volume holds state.
|
|
416
|
+
escaped = config_json.replace("'", "'\\''")
|
|
417
|
+
exit_code, output = container.exec_run(
|
|
418
|
+
cmd=["sh", "-c", f"mkdir -p /agent && printf '%s' '{escaped}' > /agent/config.json"],
|
|
419
|
+
demux=False,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if exit_code != 0:
|
|
423
|
+
logger.warning(
|
|
424
|
+
"Config write exit_code=%d for %s: %s",
|
|
425
|
+
exit_code, agent_name, output,
|
|
426
|
+
)
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
logger.info("Agent config written to container %s", container_name)
|
|
430
|
+
return True
|
|
431
|
+
|
|
432
|
+
def start(
|
|
433
|
+
self,
|
|
434
|
+
agent_name: str,
|
|
435
|
+
provision_result: Dict[str, Any],
|
|
436
|
+
) -> bool:
|
|
437
|
+
"""Start the agent container.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
agent_name: Agent instance name.
|
|
441
|
+
provision_result: Output from provision().
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
True if the container started successfully.
|
|
445
|
+
"""
|
|
446
|
+
client = self._client()
|
|
447
|
+
container_name = provision_result.get("container_name", "")
|
|
448
|
+
if not container_name:
|
|
449
|
+
return False
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
container = client.containers.get(container_name)
|
|
453
|
+
container.start()
|
|
454
|
+
container.reload()
|
|
455
|
+
logger.info(
|
|
456
|
+
"Started container %s (id=%s)",
|
|
457
|
+
container_name, container.id[:12],
|
|
458
|
+
)
|
|
459
|
+
return True
|
|
460
|
+
except Exception as exc:
|
|
461
|
+
logger.error("Failed to start %s: %s", container_name, exc)
|
|
462
|
+
return False
|
|
463
|
+
|
|
464
|
+
def stop(
|
|
465
|
+
self,
|
|
466
|
+
agent_name: str,
|
|
467
|
+
provision_result: Dict[str, Any],
|
|
468
|
+
) -> bool:
|
|
469
|
+
"""Stop the agent container gracefully (SIGTERM then SIGKILL).
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
agent_name: Agent instance name.
|
|
473
|
+
provision_result: Output from provision().
|
|
474
|
+
|
|
475
|
+
Returns:
|
|
476
|
+
True if stopped or already not running.
|
|
477
|
+
"""
|
|
478
|
+
client = self._client()
|
|
479
|
+
container_name = provision_result.get("container_name", "")
|
|
480
|
+
if not container_name:
|
|
481
|
+
return True
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
container = client.containers.get(container_name)
|
|
485
|
+
container.stop(timeout=_GRACEFUL_STOP_TIMEOUT)
|
|
486
|
+
logger.info("Stopped container %s", container_name)
|
|
487
|
+
return True
|
|
488
|
+
except Exception as exc:
|
|
489
|
+
logger.warning("Could not stop %s: %s", container_name, exc)
|
|
490
|
+
return False
|
|
491
|
+
|
|
492
|
+
def destroy(
|
|
493
|
+
self,
|
|
494
|
+
agent_name: str,
|
|
495
|
+
provision_result: Dict[str, Any],
|
|
496
|
+
) -> bool:
|
|
497
|
+
"""Remove the container and its associated named volume.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
agent_name: Agent instance name.
|
|
501
|
+
provision_result: Output from provision().
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
True if destroyed (container and volume removed).
|
|
505
|
+
"""
|
|
506
|
+
self.stop(agent_name, provision_result)
|
|
507
|
+
|
|
508
|
+
client = self._client()
|
|
509
|
+
container_name = provision_result.get("container_name", "")
|
|
510
|
+
volume_name = provision_result.get("volume_name", "")
|
|
511
|
+
|
|
512
|
+
destroyed = True
|
|
513
|
+
|
|
514
|
+
if container_name:
|
|
515
|
+
try:
|
|
516
|
+
container = client.containers.get(container_name)
|
|
517
|
+
container.remove(v=True, force=True)
|
|
518
|
+
logger.info("Removed container %s", container_name)
|
|
519
|
+
except Exception as exc:
|
|
520
|
+
logger.warning("Could not remove container %s: %s", container_name, exc)
|
|
521
|
+
destroyed = False
|
|
522
|
+
|
|
523
|
+
if volume_name:
|
|
524
|
+
try:
|
|
525
|
+
vol = client.volumes.get(volume_name)
|
|
526
|
+
vol.remove(force=True)
|
|
527
|
+
logger.info("Removed volume %s", volume_name)
|
|
528
|
+
except Exception as exc:
|
|
529
|
+
logger.debug("Volume %s already removed or missing: %s", volume_name, exc)
|
|
530
|
+
|
|
531
|
+
return destroyed
|
|
532
|
+
|
|
533
|
+
def rotate(
|
|
534
|
+
self,
|
|
535
|
+
agent_name: str,
|
|
536
|
+
spec: AgentSpec,
|
|
537
|
+
provision_result: Dict[str, Any],
|
|
538
|
+
) -> Dict[str, Any]:
|
|
539
|
+
"""Destroy the container and redeploy fresh (rotation).
|
|
540
|
+
|
|
541
|
+
Used when an agent shows context degradation. Stops and removes
|
|
542
|
+
the old container, then provisions and starts a fresh instance
|
|
543
|
+
with the same spec and team membership.
|
|
544
|
+
|
|
545
|
+
The original named volume is removed by destroy() so the new
|
|
546
|
+
container starts with a clean /agent directory.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
agent_name: Agent instance name.
|
|
550
|
+
spec: Agent specification for the fresh container.
|
|
551
|
+
provision_result: Output from the previous provision() call;
|
|
552
|
+
used to locate the old container and to recover team_name.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
New provision_result dict from the fresh provision() call.
|
|
556
|
+
"""
|
|
557
|
+
team_name = provision_result.get("team_name", "")
|
|
558
|
+
|
|
559
|
+
self.destroy(agent_name, provision_result)
|
|
560
|
+
|
|
561
|
+
new_result = self.provision(agent_name, spec, team_name)
|
|
562
|
+
self.configure(agent_name, spec, new_result)
|
|
563
|
+
self.start(agent_name, new_result)
|
|
564
|
+
|
|
565
|
+
logger.info(
|
|
566
|
+
"Rotated agent %s (old_container=%s new_container=%s)",
|
|
567
|
+
agent_name,
|
|
568
|
+
provision_result.get("container_name", "?"),
|
|
569
|
+
new_result.get("container_name", "?"),
|
|
570
|
+
)
|
|
571
|
+
return new_result
|
|
572
|
+
|
|
573
|
+
def health_check(
|
|
574
|
+
self,
|
|
575
|
+
agent_name: str,
|
|
576
|
+
provision_result: Dict[str, Any],
|
|
577
|
+
) -> AgentStatus:
|
|
578
|
+
"""Inspect container status via docker inspect.
|
|
579
|
+
|
|
580
|
+
Args:
|
|
581
|
+
agent_name: Agent instance name.
|
|
582
|
+
provision_result: Output from provision().
|
|
583
|
+
|
|
584
|
+
Returns:
|
|
585
|
+
AgentStatus based on container State.Status.
|
|
586
|
+
"""
|
|
587
|
+
client = self._client()
|
|
588
|
+
container_name = provision_result.get("container_name", "")
|
|
589
|
+
if not container_name:
|
|
590
|
+
return AgentStatus.STOPPED
|
|
591
|
+
|
|
592
|
+
try:
|
|
593
|
+
container = client.containers.get(container_name)
|
|
594
|
+
container.reload()
|
|
595
|
+
state: str = container.status # running, exited, paused, …
|
|
596
|
+
if state == "running":
|
|
597
|
+
return AgentStatus.RUNNING
|
|
598
|
+
if state in ("exited", "dead"):
|
|
599
|
+
return AgentStatus.STOPPED
|
|
600
|
+
if state == "paused":
|
|
601
|
+
return AgentStatus.DEGRADED
|
|
602
|
+
return AgentStatus.DEGRADED
|
|
603
|
+
except Exception as exc:
|
|
604
|
+
logger.debug("health_check failed for %s: %s", container_name, exc)
|
|
605
|
+
return AgentStatus.FAILED
|
|
606
|
+
|
|
607
|
+
# ------------------------------------------------------------------
|
|
608
|
+
# Docker Compose generation
|
|
609
|
+
# ------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
def generate_compose(
|
|
612
|
+
self,
|
|
613
|
+
blueprint: BlueprintManifest,
|
|
614
|
+
output_path: Optional[Path] = None,
|
|
615
|
+
include_mcp_service: bool = False,
|
|
616
|
+
) -> str:
|
|
617
|
+
"""Generate a docker-compose.yml from a full blueprint manifest.
|
|
618
|
+
|
|
619
|
+
Each agent (and each instance when count > 1) becomes a service.
|
|
620
|
+
Resource limits, environment variables, and soul blueprint paths
|
|
621
|
+
are all included.
|
|
622
|
+
|
|
623
|
+
When ``skcomm_home`` is configured on the provider the compose
|
|
624
|
+
output includes a named ``skcomm-data`` volume and mounts it at
|
|
625
|
+
``/skcomm`` in every agent service.
|
|
626
|
+
|
|
627
|
+
When ``include_mcp_service=True`` a ``skcapstone-mcp`` sidecar
|
|
628
|
+
service is added; all agent containers receive ``SKCAPSTONE_MCP_HOST``
|
|
629
|
+
pointing at it so they can reach the MCP server.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
blueprint: The validated blueprint manifest.
|
|
633
|
+
output_path: If provided, the YAML is written to this path.
|
|
634
|
+
include_mcp_service: Add a skcapstone-mcp service that agents
|
|
635
|
+
connect to via SKCAPSTONE_MCP_HOST.
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
The docker-compose YAML string.
|
|
639
|
+
"""
|
|
640
|
+
services: Dict[str, Any] = {}
|
|
641
|
+
volumes: Dict[str, Any] = {}
|
|
642
|
+
|
|
643
|
+
# Add skcapstone MCP sidecar if requested
|
|
644
|
+
mcp_service_name = "skcapstone-mcp"
|
|
645
|
+
if include_mcp_service:
|
|
646
|
+
services[mcp_service_name] = {
|
|
647
|
+
"image": self._base_image,
|
|
648
|
+
"container_name": mcp_service_name,
|
|
649
|
+
"command": ["skcapstone", "mcp", "--stdio"],
|
|
650
|
+
"environment": {
|
|
651
|
+
"SKCAPSTONE_HOME": "/agent/skcapstone",
|
|
652
|
+
},
|
|
653
|
+
"volumes": [
|
|
654
|
+
"skcapstone-mcp-data:/agent/skcapstone",
|
|
655
|
+
],
|
|
656
|
+
"networks": [self._network_name],
|
|
657
|
+
"restart": "unless-stopped",
|
|
658
|
+
"labels": [
|
|
659
|
+
"managed_by=skcapstone",
|
|
660
|
+
f"team={blueprint.name}",
|
|
661
|
+
"role=mcp-server",
|
|
662
|
+
],
|
|
663
|
+
}
|
|
664
|
+
volumes["skcapstone-mcp-data"] = {}
|
|
665
|
+
|
|
666
|
+
has_skcomm = bool(self._skcomm_home)
|
|
667
|
+
if has_skcomm:
|
|
668
|
+
volumes["skcomm-data"] = {}
|
|
669
|
+
|
|
670
|
+
for agent_key, spec in blueprint.agents.items():
|
|
671
|
+
for idx in range(spec.count):
|
|
672
|
+
suffix = f"-{idx + 1}" if spec.count > 1 else ""
|
|
673
|
+
svc_name = f"{blueprint.slug}-{agent_key}{suffix}".replace("_", "-")
|
|
674
|
+
volume_name = self._volume_name(svc_name)
|
|
675
|
+
|
|
676
|
+
env: Dict[str, str] = {
|
|
677
|
+
"AGENT_NAME": svc_name,
|
|
678
|
+
"TEAM_NAME": blueprint.name,
|
|
679
|
+
"AGENT_ROLE": spec.role.value,
|
|
680
|
+
"AGENT_MODEL": spec.model_name or spec.model.value,
|
|
681
|
+
}
|
|
682
|
+
env.update(spec.env)
|
|
683
|
+
|
|
684
|
+
if spec.soul_blueprint:
|
|
685
|
+
env["SOUL_BLUEPRINT"] = spec.soul_blueprint
|
|
686
|
+
|
|
687
|
+
# Wire sovereign infra env vars
|
|
688
|
+
if include_mcp_service:
|
|
689
|
+
env["SKCAPSTONE_MCP_HOST"] = f"{mcp_service_name}:8765"
|
|
690
|
+
elif self._mcp_host:
|
|
691
|
+
env["SKCAPSTONE_MCP_HOST"] = self._mcp_host
|
|
692
|
+
|
|
693
|
+
if has_skcomm:
|
|
694
|
+
env["SKCOMM_HOME"] = "/skcomm"
|
|
695
|
+
|
|
696
|
+
svc_volumes = [f"{volume_name}:/agent"]
|
|
697
|
+
if has_skcomm:
|
|
698
|
+
svc_volumes.append("skcomm-data:/skcomm")
|
|
699
|
+
|
|
700
|
+
deploy_limits: Dict[str, Any] = {
|
|
701
|
+
"resources": {
|
|
702
|
+
"limits": {
|
|
703
|
+
"cpus": str(spec.resources.cores),
|
|
704
|
+
"memory": spec.resources.memory.upper(),
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
service: Dict[str, Any] = {
|
|
710
|
+
"image": self._base_image,
|
|
711
|
+
"container_name": svc_name,
|
|
712
|
+
"environment": env,
|
|
713
|
+
"volumes": svc_volumes,
|
|
714
|
+
"networks": [self._network_name],
|
|
715
|
+
"restart": "unless-stopped",
|
|
716
|
+
"deploy": deploy_limits,
|
|
717
|
+
"labels": [
|
|
718
|
+
"managed_by=skcapstone",
|
|
719
|
+
f"team={blueprint.name}",
|
|
720
|
+
f"agent={svc_name}",
|
|
721
|
+
f"role={spec.role.value}",
|
|
722
|
+
],
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if spec.depends_on and include_mcp_service:
|
|
726
|
+
# Always depend on MCP service first when included
|
|
727
|
+
service["depends_on"] = [mcp_service_name] + [
|
|
728
|
+
f"{blueprint.slug}-{dep}".replace("_", "-")
|
|
729
|
+
for dep in spec.depends_on
|
|
730
|
+
]
|
|
731
|
+
elif spec.depends_on:
|
|
732
|
+
service["depends_on"] = [
|
|
733
|
+
f"{blueprint.slug}-{dep}".replace("_", "-")
|
|
734
|
+
for dep in spec.depends_on
|
|
735
|
+
]
|
|
736
|
+
elif include_mcp_service:
|
|
737
|
+
service["depends_on"] = [mcp_service_name]
|
|
738
|
+
|
|
739
|
+
services[svc_name] = service
|
|
740
|
+
volumes[volume_name] = {}
|
|
741
|
+
|
|
742
|
+
compose: Dict[str, Any] = {
|
|
743
|
+
"version": "3.9",
|
|
744
|
+
"services": services,
|
|
745
|
+
"volumes": {k: {} for k in volumes},
|
|
746
|
+
"networks": {
|
|
747
|
+
self._network_name: {
|
|
748
|
+
"driver": "bridge",
|
|
749
|
+
}
|
|
750
|
+
},
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
compose_yaml = yaml.dump(compose, default_flow_style=False, sort_keys=False)
|
|
754
|
+
|
|
755
|
+
if output_path:
|
|
756
|
+
Path(output_path).write_text(compose_yaml, encoding="utf-8")
|
|
757
|
+
logger.info("docker-compose.yml written to %s", output_path)
|
|
758
|
+
|
|
759
|
+
return compose_yaml
|