@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,581 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for skcapstone Vault (sync/vault.py).
|
|
3
|
+
|
|
4
|
+
Covers packing, unpacking, integrity verification, key helpers,
|
|
5
|
+
exclusion logic, manifest models, and encryption paths with mocked GPG.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import tarfile
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from unittest.mock import MagicMock, patch
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Fixtures
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def agent_home(tmp_path: Path) -> Path:
|
|
27
|
+
home = tmp_path / ".skcapstone"
|
|
28
|
+
home.mkdir()
|
|
29
|
+
|
|
30
|
+
for pillar in ("identity", "memory", "trust", "config", "skills"):
|
|
31
|
+
(home / pillar).mkdir()
|
|
32
|
+
|
|
33
|
+
(home / "identity" / "identity.json").write_text(
|
|
34
|
+
json.dumps({
|
|
35
|
+
"name": "VaultTestAgent",
|
|
36
|
+
"fingerprint": "BBBB2222CCCC3333DDDD4444EEEE5555FFFF6666",
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
(home / "trust" / "trust.json").write_text(
|
|
40
|
+
json.dumps({"depth": 4.0, "trust_level": 0.85})
|
|
41
|
+
)
|
|
42
|
+
(home / "config" / "config.yaml").write_text("agent_name: VaultTestAgent\n")
|
|
43
|
+
(home / "manifest.json").write_text(
|
|
44
|
+
json.dumps({"name": "VaultTestAgent", "version": "0.2.0", "connectors": []})
|
|
45
|
+
)
|
|
46
|
+
for layer in ("short-term", "mid-term", "long-term"):
|
|
47
|
+
(home / "memory" / layer).mkdir(parents=True)
|
|
48
|
+
(home / "memory" / "long-term" / "important.json").write_text(
|
|
49
|
+
json.dumps({"content": "vault test memory"})
|
|
50
|
+
)
|
|
51
|
+
return home
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture
|
|
55
|
+
def vault(agent_home: Path):
|
|
56
|
+
from skcapstone.sync.vault import Vault
|
|
57
|
+
|
|
58
|
+
return Vault(agent_home)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Helper functions
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestHashHelpers:
|
|
67
|
+
def test_sha256_file(self, tmp_path: Path):
|
|
68
|
+
from skcapstone.sync.vault import _sha256_file
|
|
69
|
+
|
|
70
|
+
f = tmp_path / "data.bin"
|
|
71
|
+
f.write_bytes(b"hello world")
|
|
72
|
+
digest = _sha256_file(f)
|
|
73
|
+
expected = hashlib.sha256(b"hello world").hexdigest()
|
|
74
|
+
assert digest == expected
|
|
75
|
+
assert len(digest) == 64
|
|
76
|
+
|
|
77
|
+
def test_sha256_bytes(self):
|
|
78
|
+
from skcapstone.sync.vault import _sha256_bytes
|
|
79
|
+
|
|
80
|
+
digest = _sha256_bytes(b"sovereign")
|
|
81
|
+
expected = hashlib.sha256(b"sovereign").hexdigest()
|
|
82
|
+
assert digest == expected
|
|
83
|
+
assert len(digest) == 64
|
|
84
|
+
|
|
85
|
+
def test_sha256_empty_bytes(self):
|
|
86
|
+
from skcapstone.sync.vault import _sha256_bytes
|
|
87
|
+
|
|
88
|
+
digest = _sha256_bytes(b"")
|
|
89
|
+
assert digest == hashlib.sha256(b"").hexdigest()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Vault construction
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestVaultConstruction:
|
|
98
|
+
def test_creates_vault_dir(self, agent_home: Path):
|
|
99
|
+
from skcapstone.sync.vault import Vault
|
|
100
|
+
|
|
101
|
+
Vault(agent_home)
|
|
102
|
+
assert (agent_home / "vault").is_dir()
|
|
103
|
+
|
|
104
|
+
def test_expands_home_tilde(self, tmp_path: Path):
|
|
105
|
+
"""Vault should call expanduser() on agent_home."""
|
|
106
|
+
real_home = tmp_path / "agent"
|
|
107
|
+
real_home.mkdir()
|
|
108
|
+
from skcapstone.sync.vault import Vault
|
|
109
|
+
|
|
110
|
+
v = Vault(real_home)
|
|
111
|
+
assert v.agent_home == real_home
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Exclusion logic
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestShouldExclude:
|
|
120
|
+
def test_excludes_pycache(self, vault):
|
|
121
|
+
assert vault._should_exclude("__pycache__") is True
|
|
122
|
+
|
|
123
|
+
def test_excludes_pyc(self, vault):
|
|
124
|
+
assert vault._should_exclude("compiled.pyc") is True
|
|
125
|
+
|
|
126
|
+
def test_excludes_git(self, vault):
|
|
127
|
+
assert vault._should_exclude(".git") is True
|
|
128
|
+
|
|
129
|
+
def test_excludes_audit_log(self, vault):
|
|
130
|
+
assert vault._should_exclude("audit.log") is True
|
|
131
|
+
|
|
132
|
+
def test_allows_normal_files(self, vault):
|
|
133
|
+
assert vault._should_exclude("identity.json") is False
|
|
134
|
+
assert vault._should_exclude("trust.json") is False
|
|
135
|
+
assert vault._should_exclude("memories.json") is False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------------
|
|
139
|
+
# pack
|
|
140
|
+
# ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TestVaultPack:
|
|
144
|
+
def test_pack_archive_naming(self, vault):
|
|
145
|
+
"""Archive name should match vault-<host>-<timestamp>.tar.gz pattern."""
|
|
146
|
+
path = vault.pack(encrypt=False)
|
|
147
|
+
assert path.name.startswith("vault-")
|
|
148
|
+
assert path.name.endswith(".tar.gz")
|
|
149
|
+
|
|
150
|
+
def test_pack_archive_in_vault_dir(self, vault, agent_home: Path):
|
|
151
|
+
path = vault.pack(encrypt=False)
|
|
152
|
+
assert path.parent == agent_home / "vault"
|
|
153
|
+
|
|
154
|
+
def test_pack_creates_manifest_file(self, vault):
|
|
155
|
+
path = vault.pack(encrypt=False)
|
|
156
|
+
manifest = path.with_suffix(".manifest.json")
|
|
157
|
+
assert manifest.exists()
|
|
158
|
+
|
|
159
|
+
def test_manifest_agent_name_from_manifest_json(self, vault):
|
|
160
|
+
path = vault.pack(encrypt=False)
|
|
161
|
+
data = json.loads(path.with_suffix(".manifest.json").read_text())
|
|
162
|
+
assert data["agent_name"] == "VaultTestAgent"
|
|
163
|
+
|
|
164
|
+
def test_manifest_includes_all_pillars(self, vault):
|
|
165
|
+
path = vault.pack(encrypt=False)
|
|
166
|
+
data = json.loads(path.with_suffix(".manifest.json").read_text())
|
|
167
|
+
included = data["pillars_included"]
|
|
168
|
+
assert "identity" in included
|
|
169
|
+
assert "trust" in included
|
|
170
|
+
assert "config" in included
|
|
171
|
+
assert "memory" in included
|
|
172
|
+
|
|
173
|
+
def test_pack_custom_pillar_selection(self, vault):
|
|
174
|
+
"""Only requested pillars should be included."""
|
|
175
|
+
path = vault.pack(pillars=["identity", "trust"], encrypt=False)
|
|
176
|
+
with tarfile.open(path, "r:gz") as tar:
|
|
177
|
+
names = tar.getnames()
|
|
178
|
+
assert any("identity/" in n for n in names)
|
|
179
|
+
assert any("trust/" in n for n in names)
|
|
180
|
+
assert not any("config/" in n for n in names)
|
|
181
|
+
|
|
182
|
+
def test_pack_skips_missing_pillar(self, vault):
|
|
183
|
+
"""A pillar that doesn't exist on disk is silently skipped."""
|
|
184
|
+
path = vault.pack(pillars=["identity", "nonexistent"], encrypt=False)
|
|
185
|
+
data = json.loads(path.with_suffix(".manifest.json").read_text())
|
|
186
|
+
assert "identity" in data["pillars_included"]
|
|
187
|
+
assert "nonexistent" not in data["pillars_included"]
|
|
188
|
+
|
|
189
|
+
def test_pack_excludes_pycache_files(self, vault, agent_home: Path):
|
|
190
|
+
"""__pycache__ and .pyc must not appear in the archive."""
|
|
191
|
+
pycache = agent_home / "identity" / "__pycache__"
|
|
192
|
+
pycache.mkdir()
|
|
193
|
+
(pycache / "cached.pyc").write_bytes(b"junk bytecode")
|
|
194
|
+
|
|
195
|
+
path = vault.pack(encrypt=False)
|
|
196
|
+
with tarfile.open(path, "r:gz") as tar:
|
|
197
|
+
names = tar.getnames()
|
|
198
|
+
assert not any("__pycache__" in n for n in names)
|
|
199
|
+
assert not any(".pyc" in n for n in names)
|
|
200
|
+
|
|
201
|
+
def test_pack_includes_manifest_json(self, vault, agent_home: Path):
|
|
202
|
+
"""Top-level manifest.json should be bundled in the archive."""
|
|
203
|
+
path = vault.pack(encrypt=False)
|
|
204
|
+
with tarfile.open(path, "r:gz") as tar:
|
|
205
|
+
assert "manifest.json" in tar.getnames()
|
|
206
|
+
|
|
207
|
+
def test_pack_skips_manifest_if_not_present(self, vault, agent_home: Path):
|
|
208
|
+
"""Pack should not crash when manifest.json doesn't exist."""
|
|
209
|
+
(agent_home / "manifest.json").unlink()
|
|
210
|
+
path = vault.pack(encrypt=False)
|
|
211
|
+
assert path.exists()
|
|
212
|
+
|
|
213
|
+
def test_pack_file_hashes_in_manifest(self, vault):
|
|
214
|
+
path = vault.pack(encrypt=False)
|
|
215
|
+
data = json.loads(path.with_suffix(".manifest.json").read_text())
|
|
216
|
+
assert isinstance(data["file_hashes"], dict)
|
|
217
|
+
assert len(data["file_hashes"]) > 0
|
|
218
|
+
for rel_path, h in data["file_hashes"].items():
|
|
219
|
+
assert len(h) == 64, f"Bad hash length for {rel_path}"
|
|
220
|
+
|
|
221
|
+
def test_pack_archive_hash_in_manifest(self, vault):
|
|
222
|
+
from skcapstone.sync.vault import _sha256_file
|
|
223
|
+
|
|
224
|
+
path = vault.pack(encrypt=False)
|
|
225
|
+
data = json.loads(path.with_suffix(".manifest.json").read_text())
|
|
226
|
+
assert data["archive_hash"] == _sha256_file(path)
|
|
227
|
+
|
|
228
|
+
def test_pack_schema_version(self, vault):
|
|
229
|
+
path = vault.pack(encrypt=False)
|
|
230
|
+
data = json.loads(path.with_suffix(".manifest.json").read_text())
|
|
231
|
+
assert data["schema_version"] == "1.1"
|
|
232
|
+
|
|
233
|
+
def test_pack_encrypted_flag_false(self, vault):
|
|
234
|
+
path = vault.pack(encrypt=False)
|
|
235
|
+
data = json.loads(path.with_suffix(".manifest.json").read_text())
|
|
236
|
+
assert data["encrypted"] is False
|
|
237
|
+
|
|
238
|
+
def test_pack_fingerprint_from_identity(self, vault):
|
|
239
|
+
path = vault.pack(encrypt=False)
|
|
240
|
+
data = json.loads(path.with_suffix(".manifest.json").read_text())
|
|
241
|
+
assert data["fingerprint"] == "BBBB2222CCCC3333DDDD4444EEEE5555FFFF6666"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# pack + encryption (mocked GPG)
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class TestVaultPackEncrypt:
|
|
250
|
+
def test_pack_encrypt_returns_gpg_path(self, vault):
|
|
251
|
+
"""pack(encrypt=True) should return a .tar.gz.gpg path."""
|
|
252
|
+
def fake_encrypt(archive_path, passphrase):
|
|
253
|
+
# Create the .gpg output; pack() will unlink the original itself
|
|
254
|
+
gpg_path = archive_path.with_suffix(archive_path.suffix + ".gpg")
|
|
255
|
+
gpg_path.write_bytes(b"encrypted")
|
|
256
|
+
return gpg_path
|
|
257
|
+
|
|
258
|
+
with patch.object(vault, "_encrypt_vault", side_effect=fake_encrypt):
|
|
259
|
+
result = vault.pack(encrypt=True, passphrase="secret")
|
|
260
|
+
|
|
261
|
+
assert result.name.endswith(".gpg")
|
|
262
|
+
|
|
263
|
+
def test_pack_encrypt_removes_plaintext(self, vault):
|
|
264
|
+
"""The plaintext .tar.gz should be deleted after encryption."""
|
|
265
|
+
def fake_encrypt(archive_path, passphrase):
|
|
266
|
+
gpg_path = archive_path.with_suffix(archive_path.suffix + ".gpg")
|
|
267
|
+
gpg_path.write_bytes(b"encrypted")
|
|
268
|
+
return gpg_path
|
|
269
|
+
|
|
270
|
+
with patch.object(vault, "_encrypt_vault", side_effect=fake_encrypt):
|
|
271
|
+
result = vault.pack(encrypt=True, passphrase="secret")
|
|
272
|
+
|
|
273
|
+
plain = result.with_name(result.name[:-4]) # strip .gpg
|
|
274
|
+
assert not plain.exists()
|
|
275
|
+
assert result.exists()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# unpack
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class TestVaultUnpack:
|
|
284
|
+
def test_unpack_extracts_files(self, vault, tmp_path: Path):
|
|
285
|
+
archive = vault.pack(encrypt=False)
|
|
286
|
+
restore = tmp_path / "restored"
|
|
287
|
+
restore.mkdir()
|
|
288
|
+
result = vault.unpack(archive, target=restore, verify_signature=False)
|
|
289
|
+
assert result == restore
|
|
290
|
+
assert (restore / "identity" / "identity.json").exists()
|
|
291
|
+
assert (restore / "trust" / "trust.json").exists()
|
|
292
|
+
|
|
293
|
+
def test_unpack_default_target_is_agent_home(self, vault, agent_home: Path, tmp_path: Path):
|
|
294
|
+
"""Without explicit target, unpack extracts to agent_home."""
|
|
295
|
+
alt_vault = Vault_helper(agent_home)
|
|
296
|
+
archive = alt_vault.pack(encrypt=False)
|
|
297
|
+
result = alt_vault.unpack(archive, verify_signature=False)
|
|
298
|
+
assert result == agent_home
|
|
299
|
+
|
|
300
|
+
def test_unpack_without_manifest(self, vault, tmp_path: Path, agent_home: Path):
|
|
301
|
+
"""Unpack works gracefully when no manifest file exists."""
|
|
302
|
+
archive = vault.pack(encrypt=False)
|
|
303
|
+
manifest = archive.with_suffix(".manifest.json")
|
|
304
|
+
manifest.unlink()
|
|
305
|
+
|
|
306
|
+
restore = tmp_path / "no-manifest"
|
|
307
|
+
restore.mkdir()
|
|
308
|
+
result = vault.unpack(archive, target=restore, verify_signature=False)
|
|
309
|
+
assert result == restore
|
|
310
|
+
|
|
311
|
+
def test_unpack_detects_tampered_archive(self, vault, tmp_path: Path):
|
|
312
|
+
"""Appending bytes to archive triggers VaultIntegrityError."""
|
|
313
|
+
from skcapstone.sync.vault import VaultIntegrityError
|
|
314
|
+
|
|
315
|
+
archive = vault.pack(encrypt=False)
|
|
316
|
+
with open(archive, "ab") as f:
|
|
317
|
+
f.write(b"\x00TAMPERED\x00")
|
|
318
|
+
|
|
319
|
+
restore = tmp_path / "tampered"
|
|
320
|
+
restore.mkdir()
|
|
321
|
+
with pytest.raises(VaultIntegrityError, match="[Hh]ash mismatch"):
|
|
322
|
+
vault.unpack(archive, target=restore, verify_signature=False)
|
|
323
|
+
|
|
324
|
+
def test_unpack_detects_tampered_file_via_manifest(self, vault, tmp_path: Path, agent_home: Path):
|
|
325
|
+
"""Faking a file hash in manifest triggers VaultIntegrityError on extraction."""
|
|
326
|
+
from skcapstone.sync.vault import VaultIntegrityError
|
|
327
|
+
|
|
328
|
+
archive = vault.pack(encrypt=False)
|
|
329
|
+
manifest_path = archive.with_suffix(".manifest.json")
|
|
330
|
+
data = json.loads(manifest_path.read_text())
|
|
331
|
+
|
|
332
|
+
# Nullify archive_hash to pass that check, corrupt a file hash
|
|
333
|
+
data["archive_hash"] = None
|
|
334
|
+
first_key = next(iter(data["file_hashes"]))
|
|
335
|
+
data["file_hashes"][first_key] = "deadbeef" * 8
|
|
336
|
+
manifest_path.write_text(json.dumps(data))
|
|
337
|
+
|
|
338
|
+
restore = tmp_path / "file-tampered"
|
|
339
|
+
restore.mkdir()
|
|
340
|
+
with pytest.raises(VaultIntegrityError, match="[Hh]ash mismatch"):
|
|
341
|
+
vault.unpack(archive, target=restore, verify_signature=False)
|
|
342
|
+
|
|
343
|
+
def test_unpack_skip_hash_verification(self, vault, tmp_path: Path):
|
|
344
|
+
"""verify_hashes=False skips all integrity checks."""
|
|
345
|
+
archive = vault.pack(encrypt=False)
|
|
346
|
+
with open(archive, "ab") as f:
|
|
347
|
+
f.write(b"\x00CORRUPT\x00")
|
|
348
|
+
|
|
349
|
+
restore = tmp_path / "skip"
|
|
350
|
+
restore.mkdir()
|
|
351
|
+
# Should not raise
|
|
352
|
+
vault.unpack(archive, target=restore, verify_signature=False, verify_hashes=False)
|
|
353
|
+
|
|
354
|
+
def test_unpack_with_tampered_archive_hash_in_manifest(self, vault, tmp_path: Path):
|
|
355
|
+
"""If manifest archive_hash is wrong, VaultIntegrityError is raised."""
|
|
356
|
+
from skcapstone.sync.vault import VaultIntegrityError
|
|
357
|
+
|
|
358
|
+
archive = vault.pack(encrypt=False)
|
|
359
|
+
manifest_path = archive.with_suffix(".manifest.json")
|
|
360
|
+
data = json.loads(manifest_path.read_text())
|
|
361
|
+
data["archive_hash"] = "0" * 64
|
|
362
|
+
manifest_path.write_text(json.dumps(data))
|
|
363
|
+
|
|
364
|
+
restore = tmp_path / "bad-hash"
|
|
365
|
+
restore.mkdir()
|
|
366
|
+
with pytest.raises(VaultIntegrityError, match="Archive hash mismatch"):
|
|
367
|
+
vault.unpack(archive, target=restore, verify_signature=False)
|
|
368
|
+
|
|
369
|
+
def test_unpack_valid_hashes_succeeds(self, vault, tmp_path: Path):
|
|
370
|
+
archive = vault.pack(encrypt=False)
|
|
371
|
+
restore = tmp_path / "valid"
|
|
372
|
+
restore.mkdir()
|
|
373
|
+
result = vault.unpack(archive, target=restore, verify_signature=False)
|
|
374
|
+
assert result == restore
|
|
375
|
+
assert (restore / "manifest.json").exists()
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# ---------------------------------------------------------------------------
|
|
379
|
+
# _get_agent_name and _get_agent_fingerprint
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class TestVaultAgentHelpers:
|
|
384
|
+
def test_get_agent_name_from_manifest(self, vault):
|
|
385
|
+
assert vault._get_agent_name() == "VaultTestAgent"
|
|
386
|
+
|
|
387
|
+
def test_get_agent_name_no_manifest(self, vault, agent_home: Path):
|
|
388
|
+
(agent_home / "manifest.json").unlink()
|
|
389
|
+
assert vault._get_agent_name() == "unknown"
|
|
390
|
+
|
|
391
|
+
def test_get_agent_name_corrupt_manifest(self, vault, agent_home: Path):
|
|
392
|
+
(agent_home / "manifest.json").write_text("NOT JSON {{{")
|
|
393
|
+
assert vault._get_agent_name() == "unknown"
|
|
394
|
+
|
|
395
|
+
def test_get_agent_fingerprint_from_identity(self, vault):
|
|
396
|
+
fp = vault._get_agent_fingerprint()
|
|
397
|
+
assert fp == "BBBB2222CCCC3333DDDD4444EEEE5555FFFF6666"
|
|
398
|
+
|
|
399
|
+
def test_get_agent_fingerprint_no_identity_file(self, vault, agent_home: Path):
|
|
400
|
+
(agent_home / "identity" / "identity.json").unlink()
|
|
401
|
+
assert vault._get_agent_fingerprint() is None
|
|
402
|
+
|
|
403
|
+
def test_get_agent_fingerprint_corrupt_identity(self, vault, agent_home: Path):
|
|
404
|
+
(agent_home / "identity" / "identity.json").write_text("{BROKEN}")
|
|
405
|
+
assert vault._get_agent_fingerprint() is None
|
|
406
|
+
|
|
407
|
+
def test_get_agent_fingerprint_no_identity_dir(self, vault, agent_home: Path):
|
|
408
|
+
import shutil
|
|
409
|
+
|
|
410
|
+
shutil.rmtree(agent_home / "identity")
|
|
411
|
+
assert vault._get_agent_fingerprint() is None
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# ---------------------------------------------------------------------------
|
|
415
|
+
# list_vaults
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class TestListVaults:
|
|
420
|
+
def test_empty_vault_dir(self, vault):
|
|
421
|
+
assert vault.list_vaults() == []
|
|
422
|
+
|
|
423
|
+
def test_single_vault(self, vault):
|
|
424
|
+
vault.pack(encrypt=False)
|
|
425
|
+
result = vault.list_vaults()
|
|
426
|
+
assert len(result) == 1
|
|
427
|
+
|
|
428
|
+
def test_two_vaults(self, vault):
|
|
429
|
+
import time
|
|
430
|
+
|
|
431
|
+
vault.pack(encrypt=False)
|
|
432
|
+
time.sleep(1.1)
|
|
433
|
+
vault.pack(encrypt=False)
|
|
434
|
+
result = vault.list_vaults()
|
|
435
|
+
assert len(result) == 2
|
|
436
|
+
|
|
437
|
+
def test_list_includes_metadata(self, vault):
|
|
438
|
+
vault.pack(encrypt=False)
|
|
439
|
+
entries = vault.list_vaults()
|
|
440
|
+
entry = entries[0]
|
|
441
|
+
assert "path" in entry
|
|
442
|
+
assert "size" in entry
|
|
443
|
+
# Manifest fields should be merged
|
|
444
|
+
assert "agent_name" in entry
|
|
445
|
+
|
|
446
|
+
def test_manifest_json_files_excluded(self, vault, agent_home: Path):
|
|
447
|
+
"""list_vaults should not count .manifest.json files as vaults."""
|
|
448
|
+
vault.pack(encrypt=False)
|
|
449
|
+
entries = vault.list_vaults()
|
|
450
|
+
for e in entries:
|
|
451
|
+
assert not str(e["path"]).endswith(".json")
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# ---------------------------------------------------------------------------
|
|
455
|
+
# VaultManifest model
|
|
456
|
+
# ---------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class TestVaultManifestModel:
|
|
460
|
+
def test_default_schema_version(self):
|
|
461
|
+
from skcapstone.sync.models import VaultManifest
|
|
462
|
+
|
|
463
|
+
m = VaultManifest(
|
|
464
|
+
agent_name="A", source_host="h", created_at=datetime.now(timezone.utc)
|
|
465
|
+
)
|
|
466
|
+
assert m.schema_version == "1.1"
|
|
467
|
+
|
|
468
|
+
def test_default_encrypted_true(self):
|
|
469
|
+
from skcapstone.sync.models import VaultManifest
|
|
470
|
+
|
|
471
|
+
m = VaultManifest(
|
|
472
|
+
agent_name="A", source_host="h", created_at=datetime.now(timezone.utc)
|
|
473
|
+
)
|
|
474
|
+
assert m.encrypted is True
|
|
475
|
+
|
|
476
|
+
def test_default_pillars_empty(self):
|
|
477
|
+
from skcapstone.sync.models import VaultManifest
|
|
478
|
+
|
|
479
|
+
m = VaultManifest(
|
|
480
|
+
agent_name="A", source_host="h", created_at=datetime.now(timezone.utc)
|
|
481
|
+
)
|
|
482
|
+
assert m.pillars_included == []
|
|
483
|
+
|
|
484
|
+
def test_default_file_hashes_empty(self):
|
|
485
|
+
from skcapstone.sync.models import VaultManifest
|
|
486
|
+
|
|
487
|
+
m = VaultManifest(
|
|
488
|
+
agent_name="A", source_host="h", created_at=datetime.now(timezone.utc)
|
|
489
|
+
)
|
|
490
|
+
assert m.file_hashes == {}
|
|
491
|
+
|
|
492
|
+
def test_roundtrip_json(self):
|
|
493
|
+
from skcapstone.sync.models import VaultManifest
|
|
494
|
+
|
|
495
|
+
m = VaultManifest(
|
|
496
|
+
agent_name="Roundtrip",
|
|
497
|
+
source_host="node01",
|
|
498
|
+
created_at=datetime(2026, 2, 28, tzinfo=timezone.utc),
|
|
499
|
+
pillars_included=["identity", "memory"],
|
|
500
|
+
encrypted=False,
|
|
501
|
+
file_hashes={"identity/identity.json": "a" * 64},
|
|
502
|
+
archive_hash="b" * 64,
|
|
503
|
+
fingerprint="FFFFFFFFFFFFFFFFFFFFFFFF",
|
|
504
|
+
)
|
|
505
|
+
restored = VaultManifest.model_validate_json(m.model_dump_json())
|
|
506
|
+
assert restored.agent_name == "Roundtrip"
|
|
507
|
+
assert restored.encrypted is False
|
|
508
|
+
assert restored.file_hashes == {"identity/identity.json": "a" * 64}
|
|
509
|
+
assert restored.archive_hash == "b" * 64
|
|
510
|
+
assert restored.fingerprint == "FFFFFFFFFFFFFFFFFFFFFFFF"
|
|
511
|
+
|
|
512
|
+
def test_signature_fields(self):
|
|
513
|
+
from skcapstone.sync.models import VaultManifest
|
|
514
|
+
|
|
515
|
+
m = VaultManifest(
|
|
516
|
+
agent_name="A",
|
|
517
|
+
source_host="h",
|
|
518
|
+
created_at=datetime.now(timezone.utc),
|
|
519
|
+
signature="SIG_BASE64",
|
|
520
|
+
signed_by="FP1234",
|
|
521
|
+
)
|
|
522
|
+
assert m.signature == "SIG_BASE64"
|
|
523
|
+
assert m.signed_by == "FP1234"
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ---------------------------------------------------------------------------
|
|
527
|
+
# VaultIntegrityError / VaultSignatureError exceptions
|
|
528
|
+
# ---------------------------------------------------------------------------
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class TestVaultExceptions:
|
|
532
|
+
def test_integrity_error_is_exception(self):
|
|
533
|
+
from skcapstone.sync.vault import VaultIntegrityError
|
|
534
|
+
|
|
535
|
+
err = VaultIntegrityError("test")
|
|
536
|
+
assert isinstance(err, Exception)
|
|
537
|
+
assert str(err) == "test"
|
|
538
|
+
|
|
539
|
+
def test_signature_error_is_exception(self):
|
|
540
|
+
from skcapstone.sync.vault import VaultSignatureError
|
|
541
|
+
|
|
542
|
+
err = VaultSignatureError("bad sig")
|
|
543
|
+
assert isinstance(err, Exception)
|
|
544
|
+
|
|
545
|
+
def test_signature_error_raised_on_invalid_signature(self, vault, tmp_path: Path):
|
|
546
|
+
"""unpack should raise VaultSignatureError for manifests with a bad signature."""
|
|
547
|
+
from skcapstone.sync.vault import VaultSignatureError
|
|
548
|
+
|
|
549
|
+
archive = vault.pack(encrypt=False)
|
|
550
|
+
manifest_path = archive.with_suffix(".manifest.json")
|
|
551
|
+
data = json.loads(manifest_path.read_text())
|
|
552
|
+
data["signature"] = "BADSIG"
|
|
553
|
+
data["archive_hash"] = None # skip archive hash check
|
|
554
|
+
manifest_path.write_text(json.dumps(data))
|
|
555
|
+
|
|
556
|
+
restore = tmp_path / "badsig"
|
|
557
|
+
restore.mkdir()
|
|
558
|
+
with patch.object(vault, "_verify_signature", return_value=False):
|
|
559
|
+
with pytest.raises(VaultSignatureError):
|
|
560
|
+
vault.unpack(
|
|
561
|
+
archive, target=restore, verify_signature=True, verify_hashes=False
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
def test_no_signature_does_not_raise(self, vault, tmp_path: Path):
|
|
565
|
+
"""A manifest with no signature should not trigger VaultSignatureError."""
|
|
566
|
+
archive = vault.pack(encrypt=False)
|
|
567
|
+
restore = tmp_path / "nosig"
|
|
568
|
+
restore.mkdir()
|
|
569
|
+
# Should not raise even with verify_signature=True
|
|
570
|
+
vault.unpack(archive, target=restore, verify_signature=True, verify_hashes=False)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# ---------------------------------------------------------------------------
|
|
574
|
+
# Helpers for test reuse
|
|
575
|
+
# ---------------------------------------------------------------------------
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def Vault_helper(agent_home: Path):
|
|
579
|
+
from skcapstone.sync.vault import Vault
|
|
580
|
+
|
|
581
|
+
return Vault(agent_home)
|