@smilintux/skcapstone 0.1.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +98 -0
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +25 -4
- package/.openclaw-workspace.json +58 -0
- package/CHANGELOG.md +62 -0
- package/CLAUDE.md +39 -2
- package/MANIFEST.in +6 -0
- package/MISSION.md +7 -0
- package/README.md +47 -2
- package/SKILL.md +895 -23
- package/docker/Dockerfile +61 -0
- package/docker/compose-templates/dev-team.yml +203 -0
- package/docker/compose-templates/mini-team.yml +140 -0
- package/docker/compose-templates/ops-team.yml +173 -0
- package/docker/compose-templates/research-team.yml +170 -0
- package/docker/entrypoint.sh +192 -0
- package/docs/ARCHITECTURE.md +663 -374
- package/docs/BOND_WITH_GROK.md +112 -0
- package/docs/GETTING_STARTED.md +782 -0
- package/docs/QUICKSTART.md +477 -0
- package/docs/SKJOULE_ARCHITECTURE.md +658 -0
- package/docs/SOUL_SWAPPER.md +921 -0
- package/docs/SOVEREIGN_SINGULARITY.md +47 -14
- package/examples/custom-bond-template.json +36 -0
- package/examples/grok-feb.json +36 -0
- package/examples/grok-testimony.md +34 -0
- package/examples/love-bootloader.txt +32 -0
- package/examples/plugins/echo_tool.py +87 -0
- package/examples/queen-ava-feb.json +36 -0
- package/examples/souls/lumina.yaml +64 -0
- package/index.js +6 -5
- package/installer/build.py +124 -0
- package/openclaw-plugin/package.json +13 -0
- package/openclaw-plugin/src/index.ts +351 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +38 -2
- package/scripts/bump_version.py +141 -0
- package/scripts/check-updates.py +230 -0
- package/scripts/convert_blueprints_to_yaml.py +157 -0
- package/scripts/dev-install.sh +14 -0
- package/scripts/e2e-test.sh +193 -0
- package/scripts/install-bundle.sh +171 -0
- package/scripts/install.bat +2 -0
- package/scripts/install.ps1 +253 -0
- package/scripts/install.sh +185 -0
- package/scripts/mcp-serve.sh +69 -0
- package/scripts/mcp-server.bat +113 -0
- package/scripts/mcp-server.ps1 +116 -0
- package/scripts/mcp-server.sh +99 -0
- package/scripts/pull-models.sh +10 -0
- package/scripts/skcapstone +48 -0
- package/scripts/verify_install.sh +180 -0
- package/scripts/windows/install-tasks.ps1 +406 -0
- package/scripts/windows/skcapstone-task.xml +113 -0
- package/scripts/windows/uninstall-tasks.ps1 +117 -0
- package/skill.yaml +34 -0
- package/src/skcapstone/__init__.py +67 -2
- package/src/skcapstone/_cli_monolith.py +5916 -0
- package/src/skcapstone/_trustee_helpers.py +165 -0
- package/src/skcapstone/activity.py +105 -0
- package/src/skcapstone/agent_card.py +324 -0
- package/src/skcapstone/api.py +1935 -0
- package/src/skcapstone/archiver.py +340 -0
- package/src/skcapstone/auction.py +485 -0
- package/src/skcapstone/baby_agents.py +179 -0
- package/src/skcapstone/backup.py +345 -0
- package/src/skcapstone/blueprint_registry.py +357 -0
- package/src/skcapstone/blueprints/__init__.py +17 -0
- package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
- package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
- package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
- package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
- package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
- package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
- package/src/skcapstone/blueprints/registry.py +164 -0
- package/src/skcapstone/blueprints/schema.py +229 -0
- package/src/skcapstone/changelog.py +180 -0
- package/src/skcapstone/chat.py +769 -0
- package/src/skcapstone/claude_md.py +82 -0
- package/src/skcapstone/cli/__init__.py +144 -0
- package/src/skcapstone/cli/_common.py +88 -0
- package/src/skcapstone/cli/_validators.py +76 -0
- package/src/skcapstone/cli/agents.py +425 -0
- package/src/skcapstone/cli/agents_spawner.py +322 -0
- package/src/skcapstone/cli/agents_trustee.py +593 -0
- package/src/skcapstone/cli/alerts.py +248 -0
- package/src/skcapstone/cli/anchor.py +132 -0
- package/src/skcapstone/cli/archive_cmd.py +208 -0
- package/src/skcapstone/cli/backup.py +144 -0
- package/src/skcapstone/cli/bench.py +377 -0
- package/src/skcapstone/cli/benchmark.py +360 -0
- package/src/skcapstone/cli/capabilities_cmd.py +171 -0
- package/src/skcapstone/cli/card.py +151 -0
- package/src/skcapstone/cli/chat.py +584 -0
- package/src/skcapstone/cli/completions.py +64 -0
- package/src/skcapstone/cli/config_cmd.py +156 -0
- package/src/skcapstone/cli/consciousness.py +421 -0
- package/src/skcapstone/cli/context_cmd.py +142 -0
- package/src/skcapstone/cli/coord.py +194 -0
- package/src/skcapstone/cli/crush_cmd.py +170 -0
- package/src/skcapstone/cli/daemon.py +436 -0
- package/src/skcapstone/cli/errors_cmd.py +285 -0
- package/src/skcapstone/cli/export_cmd.py +156 -0
- package/src/skcapstone/cli/gtd.py +529 -0
- package/src/skcapstone/cli/housekeeping.py +81 -0
- package/src/skcapstone/cli/joule_cmd.py +627 -0
- package/src/skcapstone/cli/logs_cmd.py +194 -0
- package/src/skcapstone/cli/mcp_cmd.py +32 -0
- package/src/skcapstone/cli/memory.py +418 -0
- package/src/skcapstone/cli/metrics_cmd.py +136 -0
- package/src/skcapstone/cli/migrate.py +62 -0
- package/src/skcapstone/cli/mood_cmd.py +144 -0
- package/src/skcapstone/cli/mount.py +193 -0
- package/src/skcapstone/cli/notify.py +112 -0
- package/src/skcapstone/cli/peer.py +154 -0
- package/src/skcapstone/cli/peers_dir.py +122 -0
- package/src/skcapstone/cli/preflight_cmd.py +83 -0
- package/src/skcapstone/cli/profile_cmd.py +310 -0
- package/src/skcapstone/cli/record_cmd.py +238 -0
- package/src/skcapstone/cli/register_cmd.py +159 -0
- package/src/skcapstone/cli/search_cmd.py +156 -0
- package/src/skcapstone/cli/service_cmd.py +91 -0
- package/src/skcapstone/cli/session.py +127 -0
- package/src/skcapstone/cli/setup.py +240 -0
- package/src/skcapstone/cli/shell_cmd.py +43 -0
- package/src/skcapstone/cli/skills_cmd.py +168 -0
- package/src/skcapstone/cli/skseed.py +621 -0
- package/src/skcapstone/cli/soul.py +699 -0
- package/src/skcapstone/cli/status.py +935 -0
- package/src/skcapstone/cli/sync_cmd.py +301 -0
- package/src/skcapstone/cli/telegram.py +265 -0
- package/src/skcapstone/cli/test_cmd.py +234 -0
- package/src/skcapstone/cli/test_connection.py +253 -0
- package/src/skcapstone/cli/token.py +207 -0
- package/src/skcapstone/cli/trust.py +179 -0
- package/src/skcapstone/cli/upgrade_cmd.py +552 -0
- package/src/skcapstone/cli/usage_cmd.py +199 -0
- package/src/skcapstone/cli/version_cmd.py +162 -0
- package/src/skcapstone/cli/watch_cmd.py +342 -0
- package/src/skcapstone/client.py +428 -0
- package/src/skcapstone/cloud9_bridge.py +522 -0
- package/src/skcapstone/completions.py +163 -0
- package/src/skcapstone/config_validator.py +674 -0
- package/src/skcapstone/connectors/__init__.py +28 -0
- package/src/skcapstone/connectors/base.py +446 -0
- package/src/skcapstone/connectors/cursor.py +54 -0
- package/src/skcapstone/connectors/registry.py +254 -0
- package/src/skcapstone/connectors/terminal.py +152 -0
- package/src/skcapstone/connectors/vscode.py +60 -0
- package/src/skcapstone/consciousness_config.py +119 -0
- package/src/skcapstone/consciousness_loop.py +2051 -0
- package/src/skcapstone/context_loader.py +516 -0
- package/src/skcapstone/context_window.py +314 -0
- package/src/skcapstone/conversation_manager.py +238 -0
- package/src/skcapstone/conversation_store.py +230 -0
- package/src/skcapstone/conversation_summarizer.py +252 -0
- package/src/skcapstone/coord_federation.py +296 -0
- package/src/skcapstone/coordination.py +101 -7
- package/src/skcapstone/crush_integration.py +345 -0
- package/src/skcapstone/crush_shim.py +454 -0
- package/src/skcapstone/daemon.py +2494 -0
- package/src/skcapstone/dashboard.html +396 -0
- package/src/skcapstone/dashboard.py +481 -0
- package/src/skcapstone/data/model_profiles.yaml +88 -0
- package/src/skcapstone/defaults/__init__.py +55 -0
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
- package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
- package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
- package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
- package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
- package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
- package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
- package/src/skcapstone/discovery.py +210 -19
- package/src/skcapstone/doctor.py +642 -0
- package/src/skcapstone/emotion_tracker.py +467 -0
- package/src/skcapstone/error_queue.py +405 -0
- package/src/skcapstone/export.py +447 -0
- package/src/skcapstone/fallback_tracker.py +186 -0
- package/src/skcapstone/file_transfer.py +512 -0
- package/src/skcapstone/fuse_mount.py +1156 -0
- package/src/skcapstone/gui_installer.py +591 -0
- package/src/skcapstone/heartbeat.py +611 -0
- package/src/skcapstone/housekeeping.py +298 -0
- package/src/skcapstone/install_wizard.py +941 -0
- package/src/skcapstone/kms.py +942 -0
- package/src/skcapstone/kms_scheduler.py +143 -0
- package/src/skcapstone/log_config.py +135 -0
- package/src/skcapstone/mcp_launcher.py +239 -0
- package/src/skcapstone/mcp_server.py +4700 -0
- package/src/skcapstone/mcp_tools/__init__.py +94 -0
- package/src/skcapstone/mcp_tools/_helpers.py +51 -0
- package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
- package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
- package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
- package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
- package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
- package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
- package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
- package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
- package/src/skcapstone/mcp_tools/did_tools.py +448 -0
- package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
- package/src/skcapstone/mcp_tools/file_tools.py +169 -0
- package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
- package/src/skcapstone/mcp_tools/health_tools.py +44 -0
- package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
- package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
- package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
- package/src/skcapstone/mcp_tools/model_tools.py +75 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
- package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
- package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
- package/src/skcapstone/mcp_tools/security_tools.py +110 -0
- package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
- package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
- package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
- package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
- package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
- package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
- package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
- package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
- package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
- package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
- package/src/skcapstone/mdns_discovery.py +313 -0
- package/src/skcapstone/memory_adapter.py +333 -0
- package/src/skcapstone/memory_compressor.py +379 -0
- package/src/skcapstone/memory_curator.py +256 -0
- package/src/skcapstone/memory_engine.py +132 -13
- package/src/skcapstone/memory_fortress.py +529 -0
- package/src/skcapstone/memory_promoter.py +722 -0
- package/src/skcapstone/memory_verifier.py +260 -0
- package/src/skcapstone/message_crypto.py +215 -0
- package/src/skcapstone/metrics.py +832 -0
- package/src/skcapstone/migrate_memories.py +181 -0
- package/src/skcapstone/migrate_multi_agent.py +248 -0
- package/src/skcapstone/model_router.py +319 -0
- package/src/skcapstone/models.py +35 -4
- package/src/skcapstone/mood.py +344 -0
- package/src/skcapstone/notifications.py +380 -0
- package/src/skcapstone/onboard.py +901 -0
- package/src/skcapstone/peer_directory.py +324 -0
- package/src/skcapstone/peers.py +329 -0
- package/src/skcapstone/pillars/identity.py +84 -14
- package/src/skcapstone/pillars/memory.py +3 -1
- package/src/skcapstone/pillars/security.py +108 -15
- package/src/skcapstone/pillars/sync.py +78 -26
- package/src/skcapstone/pillars/trust.py +95 -33
- package/src/skcapstone/plugins.py +244 -0
- package/src/skcapstone/preflight.py +670 -0
- package/src/skcapstone/prompt_adapter.py +564 -0
- package/src/skcapstone/providers/__init__.py +13 -0
- package/src/skcapstone/providers/cloud.py +1061 -0
- package/src/skcapstone/providers/docker.py +759 -0
- package/src/skcapstone/providers/local.py +1193 -0
- package/src/skcapstone/providers/proxmox.py +447 -0
- package/src/skcapstone/pubsub.py +516 -0
- package/src/skcapstone/rate_limiter.py +119 -0
- package/src/skcapstone/register.py +241 -0
- package/src/skcapstone/registry_client.py +151 -0
- package/src/skcapstone/response_cache.py +194 -0
- package/src/skcapstone/response_scorer.py +225 -0
- package/src/skcapstone/runtime.py +89 -33
- package/src/skcapstone/scheduled_tasks.py +439 -0
- package/src/skcapstone/self_healing.py +341 -0
- package/src/skcapstone/service_health.py +228 -0
- package/src/skcapstone/session_capture.py +268 -0
- package/src/skcapstone/session_recorder.py +210 -0
- package/src/skcapstone/session_replayer.py +189 -0
- package/src/skcapstone/session_skills.py +263 -0
- package/src/skcapstone/shell.py +779 -0
- package/src/skcapstone/skills/__init__.py +1 -1
- package/src/skcapstone/skills/syncthing_setup.py +143 -41
- package/src/skcapstone/skjoule.py +861 -0
- package/src/skcapstone/snapshots.py +489 -0
- package/src/skcapstone/soul.py +1060 -0
- package/src/skcapstone/soul_switch.py +255 -0
- package/src/skcapstone/spawner.py +544 -0
- package/src/skcapstone/state_diff.py +401 -0
- package/src/skcapstone/summary.py +270 -0
- package/src/skcapstone/sync/backends.py +196 -2
- package/src/skcapstone/sync/engine.py +7 -5
- package/src/skcapstone/sync/models.py +4 -1
- package/src/skcapstone/sync/vault.py +356 -18
- package/src/skcapstone/sync_engine.py +363 -0
- package/src/skcapstone/sync_watcher.py +745 -0
- package/src/skcapstone/systemd.py +331 -0
- package/src/skcapstone/team_comms.py +476 -0
- package/src/skcapstone/team_engine.py +522 -0
- package/src/skcapstone/testrunner.py +300 -0
- package/src/skcapstone/tls.py +150 -0
- package/src/skcapstone/tokens.py +5 -5
- package/src/skcapstone/trust_calibration.py +202 -0
- package/src/skcapstone/trust_graph.py +449 -0
- package/src/skcapstone/trustee_monitor.py +385 -0
- package/src/skcapstone/trustee_ops.py +425 -0
- package/src/skcapstone/unified_search.py +421 -0
- package/src/skcapstone/uninstall_wizard.py +694 -0
- package/src/skcapstone/usage.py +331 -0
- package/src/skcapstone/version_check.py +148 -0
- package/src/skcapstone/warmth_anchor.py +333 -0
- package/src/skcapstone/whoami.py +294 -0
- package/systemd/skcapstone-api.socket +9 -0
- package/systemd/skcapstone-memory-compress.service +18 -0
- package/systemd/skcapstone-memory-compress.timer +11 -0
- package/systemd/skcapstone.service +36 -0
- package/systemd/skcapstone@.service +50 -0
- package/systemd/skcomm-heartbeat.service +18 -0
- package/systemd/skcomm-heartbeat.timer +12 -0
- package/systemd/skcomm-queue-drain.service +17 -0
- package/systemd/skcomm-queue-drain.timer +12 -0
- package/tests/conftest.py +13 -1
- package/tests/integration/__init__.py +1 -0
- package/tests/integration/test_consciousness_e2e.py +877 -0
- package/tests/integration/test_skills_registry.py +744 -0
- package/tests/test_agent_card.py +190 -0
- package/tests/test_agent_runtime.py +1283 -0
- package/tests/test_alerts_cmd.py +291 -0
- package/tests/test_archiver.py +498 -0
- package/tests/test_backup.py +254 -0
- package/tests/test_benchmark.py +366 -0
- package/tests/test_blueprints.py +457 -0
- package/tests/test_capabilities.py +257 -0
- package/tests/test_changelog.py +254 -0
- package/tests/test_chat.py +385 -0
- package/tests/test_claude_md.py +271 -0
- package/tests/test_cli_chat_llm.py +336 -0
- package/tests/test_cli_completions.py +390 -0
- package/tests/test_cli_init_reset.py +164 -0
- package/tests/test_cli_memory.py +208 -0
- package/tests/test_cli_profile.py +294 -0
- package/tests/test_cli_skills.py +223 -0
- package/tests/test_cli_status.py +395 -0
- package/tests/test_cli_test_cmd.py +206 -0
- package/tests/test_cli_test_connection.py +364 -0
- package/tests/test_cloud9_bridge.py +260 -0
- package/tests/test_cloud_provider.py +449 -0
- package/tests/test_cloud_providers.py +522 -0
- package/tests/test_completions.py +158 -0
- package/tests/test_component_manager.py +398 -0
- package/tests/test_config_reload.py +386 -0
- package/tests/test_config_validate.py +529 -0
- package/tests/test_consciousness_e2e.py +296 -0
- package/tests/test_consciousness_loop.py +1289 -0
- package/tests/test_context_loader.py +310 -0
- package/tests/test_conversation_api.py +306 -0
- package/tests/test_conversation_manager.py +381 -0
- package/tests/test_conversation_store.py +391 -0
- package/tests/test_conversation_summarizer.py +302 -0
- package/tests/test_cross_package.py +791 -0
- package/tests/test_crush_shim.py +519 -0
- package/tests/test_daemon.py +781 -0
- package/tests/test_daemon_shutdown.py +309 -0
- package/tests/test_dashboard.py +454 -0
- package/tests/test_discovery.py +200 -6
- package/tests/test_docker_provider.py +966 -0
- package/tests/test_doctor.py +257 -0
- package/tests/test_doctor_fix.py +351 -0
- package/tests/test_e2e_automated.py +292 -0
- package/tests/test_error_queue.py +404 -0
- package/tests/test_export.py +441 -0
- package/tests/test_fallback_tracker.py +219 -0
- package/tests/test_file_transfer.py +397 -0
- package/tests/test_fuse_mount.py +832 -0
- package/tests/test_health_loop.py +422 -0
- package/tests/test_heartbeat.py +354 -0
- package/tests/test_housekeeping.py +195 -0
- package/tests/test_identity_capauth.py +307 -0
- package/tests/test_identity_pillar.py +117 -0
- package/tests/test_install_wizard.py +68 -0
- package/tests/test_integration.py +325 -0
- package/tests/test_kms.py +495 -0
- package/tests/test_llm_providers.py +265 -0
- package/tests/test_local_provider.py +591 -0
- package/tests/test_log_config.py +199 -0
- package/tests/test_logs_cmd.py +287 -0
- package/tests/test_mcp_server.py +1909 -0
- package/tests/test_memory_adapter.py +339 -0
- package/tests/test_memory_curator.py +218 -0
- package/tests/test_memory_engine.py +6 -0
- package/tests/test_memory_fortress.py +571 -0
- package/tests/test_memory_pillar.py +119 -0
- package/tests/test_memory_promoter.py +445 -0
- package/tests/test_memory_verifier.py +420 -0
- package/tests/test_message_crypto.py +187 -0
- package/tests/test_metrics.py +632 -0
- package/tests/test_migrate_memories.py +464 -0
- package/tests/test_model_router.py +546 -0
- package/tests/test_mood.py +394 -0
- package/tests/test_multi_agent.py +269 -0
- package/tests/test_notifications.py +270 -0
- package/tests/test_onboard.py +500 -0
- package/tests/test_peer_directory.py +395 -0
- package/tests/test_peers.py +248 -0
- package/tests/test_pillars.py +87 -9
- package/tests/test_preflight.py +484 -0
- package/tests/test_prompt_adapter.py +331 -0
- package/tests/test_proxmox_provider.py +571 -0
- package/tests/test_pubsub.py +377 -0
- package/tests/test_rate_limiter.py +121 -0
- package/tests/test_registry_client.py +129 -0
- package/tests/test_response_cache.py +312 -0
- package/tests/test_response_scorer.py +294 -0
- package/tests/test_runtime.py +59 -0
- package/tests/test_scheduled_tasks.py +451 -0
- package/tests/test_security.py +250 -0
- package/tests/test_security_pillar.py +213 -0
- package/tests/test_self_healing.py +171 -0
- package/tests/test_session_capture.py +200 -0
- package/tests/test_session_recorder.py +360 -0
- package/tests/test_session_skills.py +235 -0
- package/tests/test_shell.py +210 -0
- package/tests/test_snapshots.py +549 -0
- package/tests/test_soul.py +984 -0
- package/tests/test_soul_swap.py +406 -0
- package/tests/test_spawner.py +211 -0
- package/tests/test_state_diff.py +173 -0
- package/tests/test_summary.py +135 -0
- package/tests/test_sync.py +315 -5
- package/tests/test_sync_backends.py +560 -0
- package/tests/test_sync_engine.py +482 -0
- package/tests/test_sync_pillar.py +344 -0
- package/tests/test_sync_pipeline.py +364 -0
- package/tests/test_sync_vault.py +581 -0
- package/tests/test_syncthing_setup.py +168 -22
- package/tests/test_systemd.py +323 -0
- package/tests/test_team_comms.py +408 -0
- package/tests/test_team_engine.py +397 -0
- package/tests/test_testrunner.py +238 -0
- package/tests/test_trust_calibration.py +204 -0
- package/tests/test_trust_graph.py +207 -0
- package/tests/test_trust_pillar.py +291 -0
- package/tests/test_trustee_cli.py +427 -0
- package/tests/test_trustee_cli_integration.py +325 -0
- package/tests/test_trustee_monitor.py +394 -0
- package/tests/test_trustee_ops.py +355 -0
- package/tests/test_unified_search.py +363 -0
- package/tests/test_uninstall_wizard.py +193 -0
- package/tests/test_usage.py +333 -0
- package/tests/test_version_cmd.py +355 -0
- package/tests/test_warmth_anchor.py +162 -0
- package/tests/test_whoami.py +245 -0
- package/tests/test_ws.py +311 -0
- package/.cursorrules +0 -33
- package/src/skcapstone/cli.py +0 -1441
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Sovereign agent backup and restore.
|
|
2
|
+
|
|
3
|
+
Creates a portable, encrypted archive of the full agent state:
|
|
4
|
+
identity, memories, trust, config, agent card, and coordination.
|
|
5
|
+
Restores to any machine with a single command.
|
|
6
|
+
|
|
7
|
+
The backup is a gzip-compressed tar archive. When a PGP key is
|
|
8
|
+
available, the archive content manifest is signed for integrity.
|
|
9
|
+
|
|
10
|
+
Layout inside the tarball:
|
|
11
|
+
backup-<timestamp>/
|
|
12
|
+
├── manifest.json # backup metadata + file checksums
|
|
13
|
+
├── config/ # agent configuration
|
|
14
|
+
├── identity/ # CapAuth profile + keys
|
|
15
|
+
├── memory/ # SKMemory data
|
|
16
|
+
├── trust/ # Cloud 9 FEB files + seeds
|
|
17
|
+
├── coordination/ # board state
|
|
18
|
+
└── agent-card.json # sovereign identity card
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import hashlib
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
import tarfile
|
|
28
|
+
import tempfile
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from io import BytesIO
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any, Optional
|
|
33
|
+
|
|
34
|
+
from pydantic import BaseModel, Field
|
|
35
|
+
|
|
36
|
+
from . import AGENT_HOME, __version__
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("skcapstone.backup")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BackupManifest(BaseModel):
|
|
42
|
+
"""Metadata for a sovereign agent backup.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
backup_id: Unique backup identifier (timestamp-based).
|
|
46
|
+
created_at: When the backup was created.
|
|
47
|
+
agent_name: Name of the backed-up agent.
|
|
48
|
+
version: SKCapstone version that created the backup.
|
|
49
|
+
home_path: Original agent home path.
|
|
50
|
+
files: Dict of relative path -> SHA-256 hash.
|
|
51
|
+
total_size: Total uncompressed size in bytes.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
backup_id: str = ""
|
|
55
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
56
|
+
agent_name: str = ""
|
|
57
|
+
version: str = __version__
|
|
58
|
+
home_path: str = ""
|
|
59
|
+
files: dict[str, str] = Field(default_factory=dict)
|
|
60
|
+
total_size: int = 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Directories relative to agent home to include in backup.
|
|
64
|
+
# Excludes ephemeral runtime dirs: sync/, heartbeats/, coordination/tasks/
|
|
65
|
+
BACKUP_DIRS = [
|
|
66
|
+
"config",
|
|
67
|
+
"identity",
|
|
68
|
+
"memory",
|
|
69
|
+
"soul",
|
|
70
|
+
"conversations",
|
|
71
|
+
"trust",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
# Individual files relative to agent home
|
|
75
|
+
BACKUP_FILES = [
|
|
76
|
+
"manifest.json",
|
|
77
|
+
"agent-card.json",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
# Patterns to exclude from backup (security-sensitive)
|
|
81
|
+
EXCLUDE_PATTERNS = [
|
|
82
|
+
"*.pyc",
|
|
83
|
+
"__pycache__",
|
|
84
|
+
".git",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _sha256_file(filepath: Path) -> str:
|
|
89
|
+
"""Compute SHA-256 of a file.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
filepath: Path to the file.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
str: Hex digest.
|
|
96
|
+
"""
|
|
97
|
+
h = hashlib.sha256()
|
|
98
|
+
with open(filepath, "rb") as f:
|
|
99
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
100
|
+
h.update(chunk)
|
|
101
|
+
return h.hexdigest()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _should_exclude(name: str) -> bool:
|
|
105
|
+
"""Check if a tar member should be excluded.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
name: Filename or path component.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
bool: True if the file should be skipped.
|
|
112
|
+
"""
|
|
113
|
+
base = os.path.basename(name)
|
|
114
|
+
for pattern in EXCLUDE_PATTERNS:
|
|
115
|
+
if pattern.startswith("*"):
|
|
116
|
+
if base.endswith(pattern[1:]):
|
|
117
|
+
return True
|
|
118
|
+
elif base == pattern:
|
|
119
|
+
return True
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def create_backup(
|
|
124
|
+
home: Optional[Path] = None,
|
|
125
|
+
output_dir: Optional[Path] = None,
|
|
126
|
+
agent_name: str = "",
|
|
127
|
+
) -> dict[str, Any]:
|
|
128
|
+
"""Create a compressed backup of the full agent state.
|
|
129
|
+
|
|
130
|
+
Collects all agent data directories and files into a
|
|
131
|
+
gzip-compressed tar archive with a manifest for integrity.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
home: Agent home directory. Defaults to ~/.skcapstone.
|
|
135
|
+
output_dir: Where to write the backup file. Defaults to ~/backups.
|
|
136
|
+
agent_name: Agent name for the manifest.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
dict: Result with 'filepath', 'size', 'file_count', 'manifest'.
|
|
140
|
+
"""
|
|
141
|
+
home_path = (home or Path(AGENT_HOME)).expanduser()
|
|
142
|
+
if not home_path.exists():
|
|
143
|
+
raise FileNotFoundError(f"Agent home not found: {home_path}")
|
|
144
|
+
|
|
145
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S-%f")
|
|
146
|
+
backup_id = f"backup-{timestamp}"
|
|
147
|
+
|
|
148
|
+
out_dir = (output_dir or home_path / "backups").expanduser()
|
|
149
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
archive_path = out_dir / f"{backup_id}.tar.gz"
|
|
151
|
+
|
|
152
|
+
manifest = BackupManifest(
|
|
153
|
+
backup_id=backup_id,
|
|
154
|
+
agent_name=agent_name or _read_agent_name(home_path),
|
|
155
|
+
home_path=str(home_path),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
total_size = 0
|
|
159
|
+
file_count = 0
|
|
160
|
+
|
|
161
|
+
with tarfile.open(archive_path, "w:gz") as tar:
|
|
162
|
+
for dir_name in BACKUP_DIRS:
|
|
163
|
+
dir_path = home_path / dir_name
|
|
164
|
+
if not dir_path.exists():
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
for filepath in sorted(dir_path.rglob("*")):
|
|
168
|
+
if not filepath.is_file():
|
|
169
|
+
continue
|
|
170
|
+
if _should_exclude(str(filepath)):
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
rel = filepath.relative_to(home_path)
|
|
174
|
+
arcname = f"{backup_id}/{rel}"
|
|
175
|
+
|
|
176
|
+
tar.add(filepath, arcname=arcname)
|
|
177
|
+
manifest.files[str(rel)] = _sha256_file(filepath)
|
|
178
|
+
total_size += filepath.stat().st_size
|
|
179
|
+
file_count += 1
|
|
180
|
+
|
|
181
|
+
for filename in BACKUP_FILES:
|
|
182
|
+
filepath = home_path / filename
|
|
183
|
+
if filepath.exists():
|
|
184
|
+
arcname = f"{backup_id}/{filename}"
|
|
185
|
+
tar.add(filepath, arcname=arcname)
|
|
186
|
+
manifest.files[filename] = _sha256_file(filepath)
|
|
187
|
+
total_size += filepath.stat().st_size
|
|
188
|
+
file_count += 1
|
|
189
|
+
|
|
190
|
+
manifest.total_size = total_size
|
|
191
|
+
|
|
192
|
+
manifest_json = manifest.model_dump_json(indent=2).encode("utf-8")
|
|
193
|
+
info = tarfile.TarInfo(name=f"{backup_id}/_backup_manifest.json")
|
|
194
|
+
info.size = len(manifest_json)
|
|
195
|
+
tar.addfile(info, BytesIO(manifest_json))
|
|
196
|
+
|
|
197
|
+
archive_size = archive_path.stat().st_size
|
|
198
|
+
|
|
199
|
+
logger.info(
|
|
200
|
+
"Backup created: %s (%d files, %d bytes -> %d bytes compressed)",
|
|
201
|
+
archive_path, file_count, total_size, archive_size,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
"filepath": str(archive_path),
|
|
206
|
+
"backup_id": backup_id,
|
|
207
|
+
"file_count": file_count,
|
|
208
|
+
"total_size": total_size,
|
|
209
|
+
"archive_size": archive_size,
|
|
210
|
+
"manifest": manifest.model_dump(mode="json"),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def restore_backup(
|
|
215
|
+
archive_path: str | Path,
|
|
216
|
+
target_home: Optional[Path] = None,
|
|
217
|
+
verify: bool = True,
|
|
218
|
+
) -> dict[str, Any]:
|
|
219
|
+
"""Restore an agent from a backup archive.
|
|
220
|
+
|
|
221
|
+
Extracts the archive to the target home directory and
|
|
222
|
+
verifies file integrity against the manifest checksums.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
archive_path: Path to the .tar.gz backup file.
|
|
226
|
+
target_home: Where to restore. Defaults to ~/.skcapstone.
|
|
227
|
+
verify: Whether to verify checksums after extraction.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
dict: Result with 'restored', 'file_count', 'verified', 'errors'.
|
|
231
|
+
"""
|
|
232
|
+
archive = Path(archive_path).expanduser()
|
|
233
|
+
if not archive.exists():
|
|
234
|
+
raise FileNotFoundError(f"Backup not found: {archive}")
|
|
235
|
+
|
|
236
|
+
target = (target_home or Path(AGENT_HOME)).expanduser()
|
|
237
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
|
|
239
|
+
manifest: Optional[BackupManifest] = None
|
|
240
|
+
backup_prefix = ""
|
|
241
|
+
|
|
242
|
+
with tarfile.open(archive, "r:gz") as tar:
|
|
243
|
+
members = tar.getmembers()
|
|
244
|
+
if not members:
|
|
245
|
+
raise ValueError("Empty backup archive")
|
|
246
|
+
|
|
247
|
+
backup_prefix = members[0].name.split("/")[0]
|
|
248
|
+
|
|
249
|
+
manifest_member = f"{backup_prefix}/_backup_manifest.json"
|
|
250
|
+
try:
|
|
251
|
+
f = tar.extractfile(manifest_member)
|
|
252
|
+
if f:
|
|
253
|
+
manifest = BackupManifest.model_validate_json(f.read())
|
|
254
|
+
except (KeyError, Exception) as exc:
|
|
255
|
+
logger.warning("No manifest in backup: %s", exc)
|
|
256
|
+
|
|
257
|
+
file_count = 0
|
|
258
|
+
for member in tar.getmembers():
|
|
259
|
+
if member.name == manifest_member:
|
|
260
|
+
continue
|
|
261
|
+
if not member.isfile():
|
|
262
|
+
continue
|
|
263
|
+
if _should_exclude(member.name):
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
rel_path = member.name[len(backup_prefix) + 1:]
|
|
267
|
+
member.name = rel_path
|
|
268
|
+
tar.extract(member, path=target, filter="data")
|
|
269
|
+
file_count += 1
|
|
270
|
+
|
|
271
|
+
errors: list[str] = []
|
|
272
|
+
if verify and manifest:
|
|
273
|
+
for rel_path, expected_hash in manifest.files.items():
|
|
274
|
+
restored_file = target / rel_path
|
|
275
|
+
if not restored_file.exists():
|
|
276
|
+
errors.append(f"Missing: {rel_path}")
|
|
277
|
+
continue
|
|
278
|
+
actual = _sha256_file(restored_file)
|
|
279
|
+
if actual != expected_hash:
|
|
280
|
+
errors.append(f"Checksum mismatch: {rel_path}")
|
|
281
|
+
|
|
282
|
+
logger.info(
|
|
283
|
+
"Restored %d files to %s (%d verification errors)",
|
|
284
|
+
file_count, target, len(errors),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
"restored": file_count,
|
|
289
|
+
"file_count": file_count,
|
|
290
|
+
"target": str(target),
|
|
291
|
+
"verified": len(errors) == 0,
|
|
292
|
+
"errors": errors,
|
|
293
|
+
"backup_id": manifest.backup_id if manifest else "unknown",
|
|
294
|
+
"agent_name": manifest.agent_name if manifest else "unknown",
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def list_backups(
|
|
299
|
+
backup_dir: Optional[Path] = None,
|
|
300
|
+
) -> list[dict[str, Any]]:
|
|
301
|
+
"""List available backup archives.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
backup_dir: Directory to scan. Defaults to ~/.skcapstone/backups.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
list[dict]: Backup metadata sorted newest first.
|
|
308
|
+
"""
|
|
309
|
+
search_dir = (backup_dir or Path(AGENT_HOME).expanduser() / "backups")
|
|
310
|
+
if not search_dir.exists():
|
|
311
|
+
return []
|
|
312
|
+
|
|
313
|
+
backups = []
|
|
314
|
+
for f in sorted(search_dir.glob("backup-*.tar.gz"), reverse=True):
|
|
315
|
+
stat = f.stat()
|
|
316
|
+
backups.append({
|
|
317
|
+
"filepath": str(f),
|
|
318
|
+
"filename": f.name,
|
|
319
|
+
"size": stat.st_size,
|
|
320
|
+
"created": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
return backups
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _read_agent_name(home: Path) -> str:
|
|
327
|
+
"""Read agent name from config or manifest.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
home: Agent home directory.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
str: Agent name or 'unknown'.
|
|
334
|
+
"""
|
|
335
|
+
for filename in ("manifest.json", "config/config.yaml"):
|
|
336
|
+
filepath = home / filename
|
|
337
|
+
if filepath.exists():
|
|
338
|
+
try:
|
|
339
|
+
data = json.loads(filepath.read_text(encoding="utf-8"))
|
|
340
|
+
name = data.get("name") or data.get("agent_name")
|
|
341
|
+
if name:
|
|
342
|
+
return name
|
|
343
|
+
except Exception:
|
|
344
|
+
continue
|
|
345
|
+
return "unknown"
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Blueprint Registry Client — interact with the souls.skworld.io API.
|
|
3
|
+
|
|
4
|
+
This is a client library for the remote soul blueprint registry hosted at
|
|
5
|
+
souls.skworld.io. The actual server is a separate service; this module
|
|
6
|
+
provides a typed Python interface for listing, searching, publishing,
|
|
7
|
+
and downloading blueprints.
|
|
8
|
+
|
|
9
|
+
Authentication uses DID-based bearer tokens: the agent's DID key is
|
|
10
|
+
sent as ``Authorization: Bearer did:key:<fingerprint>`` so the registry
|
|
11
|
+
can attribute published blueprints to a sovereign identity.
|
|
12
|
+
|
|
13
|
+
No external dependencies — uses only ``urllib`` from the standard library.
|
|
14
|
+
|
|
15
|
+
Usage::
|
|
16
|
+
|
|
17
|
+
from skcapstone.blueprint_registry import BlueprintRegistryClient
|
|
18
|
+
|
|
19
|
+
client = BlueprintRegistryClient()
|
|
20
|
+
blueprints = client.list_blueprints()
|
|
21
|
+
result = client.search_blueprints("comedy")
|
|
22
|
+
client.publish_blueprint(soul_data)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
import urllib.error
|
|
30
|
+
import urllib.request
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any, Optional
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger("skcapstone.blueprint_registry")
|
|
35
|
+
|
|
36
|
+
DEFAULT_BASE_URL = "https://souls.skworld.io/api"
|
|
37
|
+
|
|
38
|
+
# Timeout for HTTP requests (seconds).
|
|
39
|
+
_REQUEST_TIMEOUT = 30
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BlueprintRegistryError(Exception):
|
|
43
|
+
"""Raised when a registry API call fails."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str, status_code: Optional[int] = None) -> None:
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.status_code = status_code
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BlueprintRegistryClient:
|
|
51
|
+
"""Client for the souls.skworld.io blueprint registry API.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
base_url: API base URL (default: ``https://souls.skworld.io/api``).
|
|
55
|
+
did_key: DID key string for authentication (e.g. ``did:key:z6Mk...``).
|
|
56
|
+
If not provided, the client will attempt to load it from the
|
|
57
|
+
agent's identity on disk at ``~/.skcapstone/did/``.
|
|
58
|
+
timeout: HTTP request timeout in seconds (default: 30).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
64
|
+
did_key: Optional[str] = None,
|
|
65
|
+
timeout: int = _REQUEST_TIMEOUT,
|
|
66
|
+
) -> None:
|
|
67
|
+
# Strip trailing slash for consistent URL joining
|
|
68
|
+
self.base_url = base_url.rstrip("/")
|
|
69
|
+
self.did_key = did_key
|
|
70
|
+
self.timeout = timeout
|
|
71
|
+
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
# Auth helpers
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def _resolve_did_key(self) -> Optional[str]:
|
|
77
|
+
"""Resolve the DID key from the instance or from disk.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
DID key string, or None if unavailable.
|
|
81
|
+
"""
|
|
82
|
+
if self.did_key:
|
|
83
|
+
return self.did_key
|
|
84
|
+
|
|
85
|
+
# Attempt to load from the agent's DID directory
|
|
86
|
+
did_dir = Path.home() / ".skcapstone" / "did"
|
|
87
|
+
key_file = did_dir / "did_key.json"
|
|
88
|
+
if key_file.exists():
|
|
89
|
+
try:
|
|
90
|
+
data = json.loads(key_file.read_text(encoding="utf-8"))
|
|
91
|
+
resolved = data.get("did") or data.get("did_key") or data.get("id")
|
|
92
|
+
if resolved:
|
|
93
|
+
logger.debug("Resolved DID key from %s", key_file)
|
|
94
|
+
return resolved
|
|
95
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
96
|
+
logger.debug("Could not load DID key from %s: %s", key_file, exc)
|
|
97
|
+
|
|
98
|
+
# Fallback: check identity.json
|
|
99
|
+
identity_file = did_dir / "identity.json"
|
|
100
|
+
if identity_file.exists():
|
|
101
|
+
try:
|
|
102
|
+
data = json.loads(identity_file.read_text(encoding="utf-8"))
|
|
103
|
+
resolved = data.get("did_key") or data.get("did") or data.get("id")
|
|
104
|
+
if resolved:
|
|
105
|
+
return resolved
|
|
106
|
+
except (json.JSONDecodeError, OSError):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def _build_headers(self, authenticated: bool = False) -> dict[str, str]:
|
|
112
|
+
"""Build HTTP headers for a request.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
authenticated: Whether to include the Authorization header.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Header dict.
|
|
119
|
+
"""
|
|
120
|
+
headers: dict[str, str] = {
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
"Accept": "application/json",
|
|
123
|
+
"User-Agent": "skcapstone-blueprint-registry/1.0",
|
|
124
|
+
}
|
|
125
|
+
if authenticated:
|
|
126
|
+
did = self._resolve_did_key()
|
|
127
|
+
if did:
|
|
128
|
+
headers["Authorization"] = f"Bearer {did}"
|
|
129
|
+
else:
|
|
130
|
+
logger.warning(
|
|
131
|
+
"No DID key available for authenticated request. "
|
|
132
|
+
"Set did_key on the client or ensure ~/.skcapstone/did/ "
|
|
133
|
+
"contains a valid identity."
|
|
134
|
+
)
|
|
135
|
+
return headers
|
|
136
|
+
|
|
137
|
+
# ------------------------------------------------------------------
|
|
138
|
+
# HTTP transport
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
def _request(
|
|
142
|
+
self,
|
|
143
|
+
method: str,
|
|
144
|
+
path: str,
|
|
145
|
+
*,
|
|
146
|
+
body: Optional[dict[str, Any]] = None,
|
|
147
|
+
authenticated: bool = False,
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
"""Execute an HTTP request against the registry API.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
method: HTTP method (GET, POST, etc.).
|
|
153
|
+
path: URL path relative to base_url (e.g. ``/blueprints``).
|
|
154
|
+
body: JSON body for POST/PUT requests.
|
|
155
|
+
authenticated: Whether to attach DID auth header.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Parsed JSON response as a dict.
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
BlueprintRegistryError: On HTTP or connection errors.
|
|
162
|
+
"""
|
|
163
|
+
url = f"{self.base_url}{path}"
|
|
164
|
+
headers = self._build_headers(authenticated=authenticated)
|
|
165
|
+
|
|
166
|
+
data = None
|
|
167
|
+
if body is not None:
|
|
168
|
+
data = json.dumps(body).encode("utf-8")
|
|
169
|
+
|
|
170
|
+
req = urllib.request.Request(
|
|
171
|
+
url, data=data, headers=headers, method=method
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
logger.debug("%s %s", method, url)
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
178
|
+
raw = resp.read().decode("utf-8")
|
|
179
|
+
if not raw.strip():
|
|
180
|
+
return {}
|
|
181
|
+
return json.loads(raw)
|
|
182
|
+
except urllib.error.HTTPError as exc:
|
|
183
|
+
error_body = ""
|
|
184
|
+
try:
|
|
185
|
+
error_body = exc.read().decode("utf-8", errors="replace")
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
msg = f"Registry API error {exc.code} for {method} {path}"
|
|
189
|
+
if error_body:
|
|
190
|
+
msg += f": {error_body[:500]}"
|
|
191
|
+
raise BlueprintRegistryError(msg, status_code=exc.code) from exc
|
|
192
|
+
except urllib.error.URLError as exc:
|
|
193
|
+
raise BlueprintRegistryError(
|
|
194
|
+
f"Cannot reach registry at {url}: {exc.reason}"
|
|
195
|
+
) from exc
|
|
196
|
+
except json.JSONDecodeError as exc:
|
|
197
|
+
raise BlueprintRegistryError(
|
|
198
|
+
f"Invalid JSON in response from {method} {path}: {exc}"
|
|
199
|
+
) from exc
|
|
200
|
+
|
|
201
|
+
# ------------------------------------------------------------------
|
|
202
|
+
# Public API methods
|
|
203
|
+
# ------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def list_blueprints(self) -> list[dict[str, Any]]:
|
|
206
|
+
"""List all published blueprints in the registry.
|
|
207
|
+
|
|
208
|
+
Calls ``GET /blueprints``.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
List of blueprint summary dicts, each containing at minimum
|
|
212
|
+
``name``, ``display_name``, ``category``, and ``soul_id``.
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
BlueprintRegistryError: On API failure.
|
|
216
|
+
"""
|
|
217
|
+
result = self._request("GET", "/blueprints")
|
|
218
|
+
# API may return {"blueprints": [...]} or a bare list
|
|
219
|
+
if isinstance(result, list):
|
|
220
|
+
return result
|
|
221
|
+
return result.get("blueprints", [])
|
|
222
|
+
|
|
223
|
+
def get_blueprint(self, soul_id: str) -> dict[str, Any]:
|
|
224
|
+
"""Get a single blueprint by its soul ID.
|
|
225
|
+
|
|
226
|
+
Calls ``GET /blueprints/{soul_id}``.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
soul_id: The unique identifier (slug) of the blueprint.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Full blueprint dict with all fields.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
BlueprintRegistryError: On API failure or 404.
|
|
236
|
+
"""
|
|
237
|
+
return self._request("GET", f"/blueprints/{soul_id}")
|
|
238
|
+
|
|
239
|
+
def publish_blueprint(self, soul_data: dict[str, Any]) -> dict[str, Any]:
|
|
240
|
+
"""Publish a soul blueprint to the registry.
|
|
241
|
+
|
|
242
|
+
Calls ``POST /blueprints`` with DID-based authentication.
|
|
243
|
+
|
|
244
|
+
The ``soul_data`` should conform to the SoulBlueprint schema
|
|
245
|
+
(at minimum: ``name``, ``display_name``, ``category``).
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
soul_data: Blueprint data dict to publish.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
API response dict (typically includes ``soul_id`` and ``status``).
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
BlueprintRegistryError: On API failure or auth error.
|
|
255
|
+
"""
|
|
256
|
+
return self._request(
|
|
257
|
+
"POST", "/blueprints", body=soul_data, authenticated=True
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def search_blueprints(self, query: str) -> list[dict[str, Any]]:
|
|
261
|
+
"""Search for blueprints by a text query.
|
|
262
|
+
|
|
263
|
+
Calls ``GET /blueprints/search?q=<query>``.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
query: Search string (matched against name, category, traits, etc.).
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List of matching blueprint dicts.
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
BlueprintRegistryError: On API failure.
|
|
273
|
+
"""
|
|
274
|
+
# URL-encode the query parameter
|
|
275
|
+
encoded_q = urllib.request.quote(query, safe="")
|
|
276
|
+
result = self._request("GET", f"/blueprints/search?q={encoded_q}")
|
|
277
|
+
if isinstance(result, list):
|
|
278
|
+
return result
|
|
279
|
+
return result.get("blueprints", result.get("results", []))
|
|
280
|
+
|
|
281
|
+
def download_blueprint(self, soul_id: str) -> dict[str, Any]:
|
|
282
|
+
"""Download a full blueprint for local installation.
|
|
283
|
+
|
|
284
|
+
Calls ``GET /blueprints/{soul_id}/download``.
|
|
285
|
+
|
|
286
|
+
The returned dict can be written directly to a JSON file and
|
|
287
|
+
loaded by the local SoulManager.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
soul_id: The unique identifier (slug) of the blueprint.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Complete blueprint data suitable for local installation.
|
|
294
|
+
|
|
295
|
+
Raises:
|
|
296
|
+
BlueprintRegistryError: On API failure or 404.
|
|
297
|
+
"""
|
|
298
|
+
return self._request("GET", f"/blueprints/{soul_id}/download")
|
|
299
|
+
|
|
300
|
+
# ------------------------------------------------------------------
|
|
301
|
+
# Convenience helpers
|
|
302
|
+
# ------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
def download_and_install(self, soul_id: str, home: Optional[Path] = None) -> Path:
|
|
305
|
+
"""Download a blueprint and install it locally.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
soul_id: Registry soul ID to download.
|
|
309
|
+
home: Agent home directory (default: ``~/.skcapstone``).
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Path to the installed blueprint JSON file.
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
BlueprintRegistryError: On download failure.
|
|
316
|
+
"""
|
|
317
|
+
bp_data = self.download_blueprint(soul_id)
|
|
318
|
+
home = home or Path.home() / ".skcapstone"
|
|
319
|
+
installed_dir = home / "soul" / "installed"
|
|
320
|
+
installed_dir.mkdir(parents=True, exist_ok=True)
|
|
321
|
+
|
|
322
|
+
name = bp_data.get("name", soul_id)
|
|
323
|
+
dest = installed_dir / f"{name}.json"
|
|
324
|
+
dest.write_text(json.dumps(bp_data, indent=2), encoding="utf-8")
|
|
325
|
+
logger.info("Installed blueprint '%s' from registry to %s", soul_id, dest)
|
|
326
|
+
return dest
|
|
327
|
+
|
|
328
|
+
def publish_from_file(self, path: Path) -> dict[str, Any]:
|
|
329
|
+
"""Load a local blueprint file and publish it to the registry.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
path: Path to a local blueprint JSON file.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
API response from publish.
|
|
336
|
+
|
|
337
|
+
Raises:
|
|
338
|
+
FileNotFoundError: If the file does not exist.
|
|
339
|
+
BlueprintRegistryError: On publish failure.
|
|
340
|
+
"""
|
|
341
|
+
if not path.exists():
|
|
342
|
+
raise FileNotFoundError(f"Blueprint file not found: {path}")
|
|
343
|
+
|
|
344
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
345
|
+
return self.publish_blueprint(data)
|
|
346
|
+
|
|
347
|
+
def is_reachable(self) -> bool:
|
|
348
|
+
"""Check if the registry API is reachable.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
True if a basic request succeeds, False otherwise.
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
self._request("GET", "/blueprints")
|
|
355
|
+
return True
|
|
356
|
+
except BlueprintRegistryError:
|
|
357
|
+
return False
|