@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
|
@@ -6,10 +6,18 @@ Then let the transport layer do its job.
|
|
|
6
6
|
|
|
7
7
|
The vault is a tarball of selected ~/.skcapstone/ directories,
|
|
8
8
|
encrypted with PGP (via CapAuth) and signed to prove authenticity.
|
|
9
|
+
|
|
10
|
+
Hardening guarantees:
|
|
11
|
+
- SHA-256 hashes for every file in the archive
|
|
12
|
+
- SHA-256 hash of the archive itself
|
|
13
|
+
- GPG detached signature on the manifest
|
|
14
|
+
- Integrity verification before extraction
|
|
15
|
+
- Key rotation re-encrypts all existing vaults
|
|
9
16
|
"""
|
|
10
17
|
|
|
11
18
|
from __future__ import annotations
|
|
12
19
|
|
|
20
|
+
import hashlib
|
|
13
21
|
import json
|
|
14
22
|
import logging
|
|
15
23
|
import os
|
|
@@ -28,6 +36,42 @@ PILLARS_TO_SYNC = ["identity", "memory", "trust", "config", "skills"]
|
|
|
28
36
|
EXCLUDE_PATTERNS = {"__pycache__", ".pyc", ".git", "audit.log"}
|
|
29
37
|
|
|
30
38
|
|
|
39
|
+
class VaultIntegrityError(Exception):
|
|
40
|
+
"""Raised when vault integrity verification fails."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class VaultSignatureError(Exception):
|
|
44
|
+
"""Raised when vault signature verification fails."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _sha256_file(path: Path) -> str:
|
|
48
|
+
"""Compute SHA-256 hex digest of a file.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
path: File to hash.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Hex-encoded SHA-256 digest.
|
|
55
|
+
"""
|
|
56
|
+
h = hashlib.sha256()
|
|
57
|
+
with open(path, "rb") as f:
|
|
58
|
+
for chunk in iter(lambda: f.read(8192), b""):
|
|
59
|
+
h.update(chunk)
|
|
60
|
+
return h.hexdigest()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _sha256_bytes(data: bytes) -> str:
|
|
64
|
+
"""Compute SHA-256 hex digest of bytes.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
data: Bytes to hash.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Hex-encoded SHA-256 digest.
|
|
71
|
+
"""
|
|
72
|
+
return hashlib.sha256(data).hexdigest()
|
|
73
|
+
|
|
74
|
+
|
|
31
75
|
class Vault:
|
|
32
76
|
"""Manages creation and extraction of encrypted agent state vaults.
|
|
33
77
|
|
|
@@ -57,6 +101,7 @@ class Vault:
|
|
|
57
101
|
pillars: Optional[list[str]] = None,
|
|
58
102
|
encrypt: bool = False,
|
|
59
103
|
passphrase: Optional[str] = None,
|
|
104
|
+
sign: bool = False,
|
|
60
105
|
) -> Path:
|
|
61
106
|
"""Pack agent state into a vault archive.
|
|
62
107
|
|
|
@@ -64,6 +109,7 @@ class Vault:
|
|
|
64
109
|
pillars: Which pillars to include. Defaults to all syncable pillars.
|
|
65
110
|
encrypt: Whether to GPG-encrypt the archive.
|
|
66
111
|
passphrase: Passphrase for encryption (required if encrypt=True).
|
|
112
|
+
sign: Whether to GPG-sign the manifest.
|
|
67
113
|
|
|
68
114
|
Returns:
|
|
69
115
|
Path to the created vault file (.tar.gz or .tar.gz.gpg).
|
|
@@ -75,6 +121,8 @@ class Vault:
|
|
|
75
121
|
archive_path = self.vault_dir / archive_name
|
|
76
122
|
|
|
77
123
|
included = []
|
|
124
|
+
file_hashes: dict[str, str] = {}
|
|
125
|
+
|
|
78
126
|
with tarfile.open(archive_path, "w:gz") as tar:
|
|
79
127
|
for pillar in target_pillars:
|
|
80
128
|
pillar_dir = self.agent_home / pillar
|
|
@@ -94,6 +142,7 @@ class Vault:
|
|
|
94
142
|
full_path.relative_to(self.agent_home)
|
|
95
143
|
)
|
|
96
144
|
tar.add(str(full_path), arcname=arcname)
|
|
145
|
+
file_hashes[arcname] = _sha256_file(full_path)
|
|
97
146
|
|
|
98
147
|
included.append(pillar)
|
|
99
148
|
|
|
@@ -103,6 +152,9 @@ class Vault:
|
|
|
103
152
|
str(manifest_path),
|
|
104
153
|
arcname="manifest.json",
|
|
105
154
|
)
|
|
155
|
+
file_hashes["manifest.json"] = _sha256_file(manifest_path)
|
|
156
|
+
|
|
157
|
+
archive_hash = _sha256_file(archive_path)
|
|
106
158
|
|
|
107
159
|
manifest = VaultManifest(
|
|
108
160
|
agent_name=self._get_agent_name(),
|
|
@@ -110,12 +162,21 @@ class Vault:
|
|
|
110
162
|
created_at=datetime.now(timezone.utc),
|
|
111
163
|
pillars_included=included,
|
|
112
164
|
encrypted=encrypt,
|
|
165
|
+
file_hashes=file_hashes,
|
|
166
|
+
archive_hash=archive_hash,
|
|
167
|
+
fingerprint=self._get_agent_fingerprint(),
|
|
113
168
|
)
|
|
114
169
|
|
|
170
|
+
if sign:
|
|
171
|
+
sig = self._sign_manifest(manifest, passphrase)
|
|
172
|
+
if sig:
|
|
173
|
+
manifest.signature = sig
|
|
174
|
+
manifest.signed_by = self._get_agent_fingerprint()
|
|
175
|
+
|
|
115
176
|
manifest_file = archive_path.with_suffix(".manifest.json")
|
|
116
177
|
manifest_file.write_text(
|
|
117
178
|
manifest.model_dump_json(indent=2)
|
|
118
|
-
)
|
|
179
|
+
, encoding="utf-8")
|
|
119
180
|
|
|
120
181
|
if encrypt:
|
|
121
182
|
encrypted_path = self._encrypt_vault(
|
|
@@ -126,9 +187,10 @@ class Vault:
|
|
|
126
187
|
return encrypted_path
|
|
127
188
|
|
|
128
189
|
logger.info(
|
|
129
|
-
"Vault packed: %s (%d pillars)",
|
|
190
|
+
"Vault packed: %s (%d pillars, %d files hashed)",
|
|
130
191
|
archive_path,
|
|
131
192
|
len(included),
|
|
193
|
+
len(file_hashes),
|
|
132
194
|
)
|
|
133
195
|
return archive_path
|
|
134
196
|
|
|
@@ -138,40 +200,130 @@ class Vault:
|
|
|
138
200
|
decrypt: bool = False,
|
|
139
201
|
passphrase: Optional[str] = None,
|
|
140
202
|
target: Optional[Path] = None,
|
|
203
|
+
verify_signature: bool = True,
|
|
204
|
+
verify_hashes: bool = True,
|
|
141
205
|
) -> Path:
|
|
142
206
|
"""Unpack a vault archive to restore agent state.
|
|
143
207
|
|
|
208
|
+
Verifies integrity before extraction when manifest is present.
|
|
209
|
+
|
|
144
210
|
Args:
|
|
145
211
|
vault_path: Path to the vault file.
|
|
146
212
|
decrypt: Whether the vault is GPG-encrypted.
|
|
147
213
|
passphrase: Passphrase for decryption.
|
|
148
214
|
target: Where to extract. Defaults to agent_home.
|
|
215
|
+
verify_signature: Whether to verify the manifest signature.
|
|
216
|
+
verify_hashes: Whether to verify SHA-256 file hashes.
|
|
149
217
|
|
|
150
218
|
Returns:
|
|
151
219
|
Path to the extraction directory.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
VaultSignatureError: If signature verification fails.
|
|
223
|
+
VaultIntegrityError: If hash verification fails.
|
|
152
224
|
"""
|
|
153
225
|
extract_to = target or self.agent_home
|
|
154
226
|
|
|
155
227
|
if decrypt:
|
|
156
228
|
vault_path = self._decrypt_vault(vault_path, passphrase)
|
|
157
229
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
230
|
+
manifest = self._load_and_verify_manifest(
|
|
231
|
+
vault_path, verify_signature
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if manifest and manifest.archive_hash and verify_hashes:
|
|
235
|
+
actual_hash = _sha256_file(vault_path)
|
|
236
|
+
if actual_hash != manifest.archive_hash:
|
|
237
|
+
raise VaultIntegrityError(
|
|
238
|
+
f"Archive hash mismatch: expected {manifest.archive_hash}, "
|
|
239
|
+
f"got {actual_hash}"
|
|
240
|
+
)
|
|
241
|
+
logger.info("Archive integrity verified (SHA-256)")
|
|
168
242
|
|
|
169
243
|
with tarfile.open(vault_path, "r:gz") as tar:
|
|
170
244
|
tar.extractall(path=extract_to, filter="data")
|
|
171
245
|
|
|
246
|
+
if manifest and manifest.file_hashes and verify_hashes:
|
|
247
|
+
self._verify_file_hashes(extract_to, manifest.file_hashes)
|
|
248
|
+
logger.info(
|
|
249
|
+
"All %d file hashes verified", len(manifest.file_hashes)
|
|
250
|
+
)
|
|
251
|
+
|
|
172
252
|
logger.info("Vault unpacked to %s", extract_to)
|
|
173
253
|
return extract_to
|
|
174
254
|
|
|
255
|
+
def rotate_keys(
|
|
256
|
+
self,
|
|
257
|
+
old_passphrase: Optional[str] = None,
|
|
258
|
+
new_passphrase: Optional[str] = None,
|
|
259
|
+
) -> list[Path]:
|
|
260
|
+
"""Re-encrypt all vault archives with a new passphrase.
|
|
261
|
+
|
|
262
|
+
Decrypts each .gpg vault with the old passphrase, then
|
|
263
|
+
re-encrypts with the new one. Non-encrypted vaults are
|
|
264
|
+
encrypted with the new passphrase.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
old_passphrase: Current passphrase for existing encrypted vaults.
|
|
268
|
+
new_passphrase: New passphrase for re-encryption.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of paths to re-encrypted vaults.
|
|
272
|
+
"""
|
|
273
|
+
rotated: list[Path] = []
|
|
274
|
+
|
|
275
|
+
for vault_file in sorted(self.vault_dir.glob("vault-*.tar.gz.gpg")):
|
|
276
|
+
try:
|
|
277
|
+
decrypted = self._decrypt_vault(vault_file, old_passphrase)
|
|
278
|
+
new_encrypted = self._encrypt_vault(decrypted, new_passphrase)
|
|
279
|
+
decrypted.unlink()
|
|
280
|
+
vault_file.unlink()
|
|
281
|
+
|
|
282
|
+
old_manifest = vault_file.with_name(
|
|
283
|
+
vault_file.name.replace(".tar.gz.gpg", ".tar.gz.manifest.json")
|
|
284
|
+
)
|
|
285
|
+
if old_manifest.exists():
|
|
286
|
+
data = json.loads(old_manifest.read_text(encoding="utf-8"))
|
|
287
|
+
data["encrypted"] = True
|
|
288
|
+
data["archive_hash"] = _sha256_file(new_encrypted)
|
|
289
|
+
old_manifest.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
290
|
+
|
|
291
|
+
rotated.append(new_encrypted)
|
|
292
|
+
logger.info("Rotated encryption: %s", new_encrypted.name)
|
|
293
|
+
except RuntimeError as exc:
|
|
294
|
+
logger.error(
|
|
295
|
+
"Failed to rotate %s: %s", vault_file.name, exc
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
for vault_file in sorted(self.vault_dir.glob("vault-*.tar.gz")):
|
|
299
|
+
if vault_file.name.endswith(".gpg"):
|
|
300
|
+
continue
|
|
301
|
+
manifest_file = vault_file.with_suffix(".manifest.json")
|
|
302
|
+
if manifest_file.exists():
|
|
303
|
+
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
304
|
+
if data.get("encrypted"):
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
new_encrypted = self._encrypt_vault(vault_file, new_passphrase)
|
|
309
|
+
vault_file.unlink()
|
|
310
|
+
|
|
311
|
+
if manifest_file.exists():
|
|
312
|
+
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
313
|
+
data["encrypted"] = True
|
|
314
|
+
data["archive_hash"] = _sha256_file(new_encrypted)
|
|
315
|
+
manifest_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
316
|
+
|
|
317
|
+
rotated.append(new_encrypted)
|
|
318
|
+
logger.info("Encrypted plaintext vault: %s", new_encrypted.name)
|
|
319
|
+
except (RuntimeError, OSError) as exc:
|
|
320
|
+
logger.error(
|
|
321
|
+
"Failed to encrypt %s: %s", vault_file.name, exc
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
logger.info("Key rotation complete: %d vaults rotated", len(rotated))
|
|
325
|
+
return rotated
|
|
326
|
+
|
|
175
327
|
def list_vaults(self) -> list[dict]:
|
|
176
328
|
"""List all vault archives in the vault directory.
|
|
177
329
|
|
|
@@ -183,16 +335,191 @@ class Vault:
|
|
|
183
335
|
if f.suffix == ".json":
|
|
184
336
|
continue
|
|
185
337
|
manifest_file = f.with_suffix(".manifest.json")
|
|
186
|
-
meta = {"path": f, "size": f.stat().st_size}
|
|
338
|
+
meta: dict = {"path": f, "size": f.stat().st_size}
|
|
187
339
|
if manifest_file.exists():
|
|
188
340
|
try:
|
|
189
|
-
data = json.loads(manifest_file.read_text())
|
|
341
|
+
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
190
342
|
meta.update(data)
|
|
191
343
|
except json.JSONDecodeError:
|
|
192
344
|
pass
|
|
193
345
|
vaults.append(meta)
|
|
194
346
|
return vaults
|
|
195
347
|
|
|
348
|
+
def _load_and_verify_manifest(
|
|
349
|
+
self, vault_path: Path, verify_signature: bool
|
|
350
|
+
) -> Optional[VaultManifest]:
|
|
351
|
+
"""Load manifest for a vault and optionally verify its signature.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
vault_path: Path to the vault archive.
|
|
355
|
+
verify_signature: Whether to verify the GPG signature.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
VaultManifest if found, None otherwise.
|
|
359
|
+
|
|
360
|
+
Raises:
|
|
361
|
+
VaultSignatureError: If signature is present but invalid.
|
|
362
|
+
"""
|
|
363
|
+
manifest_file = vault_path.with_suffix(".manifest.json")
|
|
364
|
+
if not manifest_file.exists():
|
|
365
|
+
base = vault_path.name
|
|
366
|
+
if base.endswith(".gpg"):
|
|
367
|
+
base = base[:-4]
|
|
368
|
+
manifest_file = vault_path.parent / (base + ".manifest.json")
|
|
369
|
+
|
|
370
|
+
if not manifest_file.exists():
|
|
371
|
+
logger.debug("No manifest found for %s", vault_path.name)
|
|
372
|
+
return None
|
|
373
|
+
|
|
374
|
+
manifest_data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
375
|
+
manifest = VaultManifest(**manifest_data)
|
|
376
|
+
|
|
377
|
+
if verify_signature and manifest.signature:
|
|
378
|
+
if not self._verify_signature(manifest):
|
|
379
|
+
raise VaultSignatureError(
|
|
380
|
+
f"Invalid signature on manifest for {vault_path.name}"
|
|
381
|
+
)
|
|
382
|
+
logger.info("Manifest signature verified")
|
|
383
|
+
|
|
384
|
+
logger.info(
|
|
385
|
+
"Restoring vault from %s (agent=%s, pillars=%s)",
|
|
386
|
+
manifest.source_host,
|
|
387
|
+
manifest.agent_name,
|
|
388
|
+
manifest.pillars_included,
|
|
389
|
+
)
|
|
390
|
+
return manifest
|
|
391
|
+
|
|
392
|
+
def _verify_file_hashes(
|
|
393
|
+
self, extract_dir: Path, expected_hashes: dict[str, str]
|
|
394
|
+
) -> None:
|
|
395
|
+
"""Verify SHA-256 hashes of extracted files.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
extract_dir: Directory where files were extracted.
|
|
399
|
+
expected_hashes: Map of relative path -> expected SHA-256 hex.
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
VaultIntegrityError: If any file hash doesn't match.
|
|
403
|
+
"""
|
|
404
|
+
for rel_path, expected_hash in expected_hashes.items():
|
|
405
|
+
file_path = extract_dir / rel_path
|
|
406
|
+
if not file_path.exists():
|
|
407
|
+
logger.warning("Expected file missing: %s", rel_path)
|
|
408
|
+
continue
|
|
409
|
+
actual_hash = _sha256_file(file_path)
|
|
410
|
+
if actual_hash != expected_hash:
|
|
411
|
+
raise VaultIntegrityError(
|
|
412
|
+
f"Hash mismatch for {rel_path}: "
|
|
413
|
+
f"expected {expected_hash}, got {actual_hash}"
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
def _sign_manifest(
|
|
417
|
+
self, manifest: VaultManifest, passphrase: Optional[str]
|
|
418
|
+
) -> Optional[str]:
|
|
419
|
+
"""Sign the manifest data with the agent's private key.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
manifest: The manifest to sign (signature field excluded).
|
|
423
|
+
passphrase: Key passphrase.
|
|
424
|
+
|
|
425
|
+
Returns:
|
|
426
|
+
Base64 signature string, or None if signing unavailable.
|
|
427
|
+
"""
|
|
428
|
+
try:
|
|
429
|
+
from capauth.crypto import get_backend
|
|
430
|
+
|
|
431
|
+
backend = get_backend()
|
|
432
|
+
private_key_path = self.agent_home / "identity" / "agent.key"
|
|
433
|
+
if not private_key_path.exists():
|
|
434
|
+
logger.debug("No private key found for signing")
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
sign_data = manifest.model_dump_json(
|
|
438
|
+
exclude={"signature", "signed_by"}
|
|
439
|
+
).encode()
|
|
440
|
+
sig = backend.sign(
|
|
441
|
+
sign_data,
|
|
442
|
+
private_key_path.read_text(encoding="utf-8"),
|
|
443
|
+
passphrase or "",
|
|
444
|
+
)
|
|
445
|
+
return sig
|
|
446
|
+
except (ImportError, Exception) as exc:
|
|
447
|
+
logger.debug("CapAuth signing unavailable: %s", exc)
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
import subprocess
|
|
451
|
+
|
|
452
|
+
private_key_path = self.agent_home / "identity" / "agent.key"
|
|
453
|
+
if not private_key_path.exists():
|
|
454
|
+
return None
|
|
455
|
+
|
|
456
|
+
sign_data = manifest.model_dump_json(
|
|
457
|
+
exclude={"signature", "signed_by"}
|
|
458
|
+
).encode()
|
|
459
|
+
|
|
460
|
+
result = subprocess.run(
|
|
461
|
+
["gpg", "--batch", "--yes", "--detach-sign", "--armor",
|
|
462
|
+
"--default-key", manifest.fingerprint or ""],
|
|
463
|
+
input=sign_data,
|
|
464
|
+
capture_output=True,
|
|
465
|
+
check=False,
|
|
466
|
+
)
|
|
467
|
+
if result.returncode == 0:
|
|
468
|
+
return result.stdout.decode()
|
|
469
|
+
except Exception as exc:
|
|
470
|
+
logger.debug("GPG signing failed: %s", exc)
|
|
471
|
+
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
def _verify_signature(self, manifest: VaultManifest) -> bool:
|
|
475
|
+
"""Verify a manifest's GPG signature.
|
|
476
|
+
|
|
477
|
+
Args:
|
|
478
|
+
manifest: Manifest with signature to verify.
|
|
479
|
+
|
|
480
|
+
Returns:
|
|
481
|
+
True if signature is valid, False otherwise.
|
|
482
|
+
"""
|
|
483
|
+
if not manifest.signature:
|
|
484
|
+
return False
|
|
485
|
+
|
|
486
|
+
try:
|
|
487
|
+
from capauth.crypto import get_backend
|
|
488
|
+
|
|
489
|
+
backend = get_backend()
|
|
490
|
+
sign_data = manifest.model_dump_json(
|
|
491
|
+
exclude={"signature", "signed_by"}
|
|
492
|
+
).encode()
|
|
493
|
+
return backend.verify(sign_data, manifest.signature)
|
|
494
|
+
except (ImportError, Exception) as exc:
|
|
495
|
+
logger.debug("CapAuth verify unavailable: %s", exc)
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
import subprocess
|
|
499
|
+
|
|
500
|
+
sign_data = manifest.model_dump_json(
|
|
501
|
+
exclude={"signature", "signed_by"}
|
|
502
|
+
).encode()
|
|
503
|
+
|
|
504
|
+
with tempfile.NamedTemporaryFile(suffix=".sig", delete=False) as sig_file:
|
|
505
|
+
sig_file.write(manifest.signature.encode())
|
|
506
|
+
sig_path = sig_file.name
|
|
507
|
+
|
|
508
|
+
try:
|
|
509
|
+
result = subprocess.run(
|
|
510
|
+
["gpg", "--batch", "--verify", sig_path, "-"],
|
|
511
|
+
input=sign_data,
|
|
512
|
+
capture_output=True,
|
|
513
|
+
check=False,
|
|
514
|
+
)
|
|
515
|
+
return result.returncode == 0
|
|
516
|
+
finally:
|
|
517
|
+
Path(sig_path).unlink(missing_ok=True)
|
|
518
|
+
except Exception as exc:
|
|
519
|
+
logger.debug("GPG verify failed: %s", exc)
|
|
520
|
+
|
|
521
|
+
return False
|
|
522
|
+
|
|
196
523
|
def _encrypt_vault(
|
|
197
524
|
self, archive_path: Path, passphrase: Optional[str]
|
|
198
525
|
) -> Path:
|
|
@@ -211,17 +538,17 @@ class Vault:
|
|
|
211
538
|
data = archive_path.read_bytes()
|
|
212
539
|
identity_file = self.agent_home / "identity" / "identity.json"
|
|
213
540
|
if identity_file.exists():
|
|
214
|
-
identity = json.loads(identity_file.read_text())
|
|
541
|
+
identity = json.loads(identity_file.read_text(encoding="utf-8"))
|
|
215
542
|
private_key_path = (
|
|
216
543
|
self.agent_home / "identity" / "agent.key"
|
|
217
544
|
)
|
|
218
545
|
if private_key_path.exists():
|
|
219
546
|
signed = backend.sign(
|
|
220
547
|
data,
|
|
221
|
-
private_key_path.read_text(),
|
|
548
|
+
private_key_path.read_text(encoding="utf-8"),
|
|
222
549
|
passphrase or "",
|
|
223
550
|
)
|
|
224
|
-
output_path.write_text(signed)
|
|
551
|
+
output_path.write_text(signed, encoding="utf-8")
|
|
225
552
|
logger.info("Vault encrypted via CapAuth")
|
|
226
553
|
return output_path
|
|
227
554
|
except (ImportError, Exception) as exc:
|
|
@@ -277,8 +604,19 @@ class Vault:
|
|
|
277
604
|
manifest = self.agent_home / "manifest.json"
|
|
278
605
|
if manifest.exists():
|
|
279
606
|
try:
|
|
280
|
-
data = json.loads(manifest.read_text())
|
|
607
|
+
data = json.loads(manifest.read_text(encoding="utf-8"))
|
|
281
608
|
return data.get("name", "unknown")
|
|
282
609
|
except json.JSONDecodeError:
|
|
283
610
|
pass
|
|
284
611
|
return "unknown"
|
|
612
|
+
|
|
613
|
+
def _get_agent_fingerprint(self) -> Optional[str]:
|
|
614
|
+
"""Read agent PGP fingerprint from identity."""
|
|
615
|
+
identity_file = self.agent_home / "identity" / "identity.json"
|
|
616
|
+
if identity_file.exists():
|
|
617
|
+
try:
|
|
618
|
+
data = json.loads(identity_file.read_text(encoding="utf-8"))
|
|
619
|
+
return data.get("fingerprint")
|
|
620
|
+
except json.JSONDecodeError:
|
|
621
|
+
pass
|
|
622
|
+
return None
|