@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,571 @@
|
|
|
1
|
+
"""Tests for Memory Fortress — integrity sealing, encryption, tamper alerts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from skcapstone.memory_fortress import (
|
|
12
|
+
MemoryFortress,
|
|
13
|
+
FortressConfig,
|
|
14
|
+
SealResult,
|
|
15
|
+
_SEAL_FIELD,
|
|
16
|
+
_SEALED_AT_FIELD,
|
|
17
|
+
_ENCRYPTED_FIELD,
|
|
18
|
+
)
|
|
19
|
+
from skcapstone.models import MemoryEntry, MemoryLayer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def home(tmp_path: Path) -> Path:
|
|
24
|
+
"""Create a minimal agent home for fortress tests."""
|
|
25
|
+
identity_dir = tmp_path / "identity"
|
|
26
|
+
identity_dir.mkdir()
|
|
27
|
+
manifest = {
|
|
28
|
+
"name": "test-agent",
|
|
29
|
+
"email": "test@skcapstone.local",
|
|
30
|
+
"fingerprint": "ABCD1234567890ABCDEF1234567890ABCDEF1234",
|
|
31
|
+
"capauth_managed": False,
|
|
32
|
+
}
|
|
33
|
+
(identity_dir / "identity.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
34
|
+
|
|
35
|
+
security_dir = tmp_path / "security"
|
|
36
|
+
security_dir.mkdir()
|
|
37
|
+
|
|
38
|
+
mem_dir = tmp_path / "memory"
|
|
39
|
+
mem_dir.mkdir()
|
|
40
|
+
for layer in ("short-term", "mid-term", "long-term"):
|
|
41
|
+
(mem_dir / layer).mkdir()
|
|
42
|
+
|
|
43
|
+
return tmp_path
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def seal_key() -> bytes:
|
|
48
|
+
"""A deterministic seal key for tests."""
|
|
49
|
+
return b"test-fortress-seal-key-32-bytes!!"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.fixture
|
|
53
|
+
def fortress(home: Path, seal_key: bytes) -> MemoryFortress:
|
|
54
|
+
"""Create an initialized MemoryFortress with explicit seal key."""
|
|
55
|
+
f = MemoryFortress(home, seal_key=seal_key)
|
|
56
|
+
f.initialize()
|
|
57
|
+
return f
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def sample_entry() -> MemoryEntry:
|
|
62
|
+
"""A sample memory entry for testing."""
|
|
63
|
+
return MemoryEntry(
|
|
64
|
+
memory_id="abc123def456",
|
|
65
|
+
content="The sovereign agent remembers everything.",
|
|
66
|
+
tags=["test", "fortress"],
|
|
67
|
+
source="test",
|
|
68
|
+
layer=MemoryLayer.SHORT_TERM,
|
|
69
|
+
importance=0.8,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Initialization
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestInitialization:
|
|
79
|
+
"""Tests for fortress setup."""
|
|
80
|
+
|
|
81
|
+
def test_initialize_creates_config(self, home: Path, seal_key: bytes) -> None:
|
|
82
|
+
"""Initialize creates fortress.json config."""
|
|
83
|
+
f = MemoryFortress(home, seal_key=seal_key)
|
|
84
|
+
config = f.initialize()
|
|
85
|
+
assert config.enabled is True
|
|
86
|
+
assert (home / "memory" / "fortress.json").exists()
|
|
87
|
+
|
|
88
|
+
def test_initialize_idempotent(self, fortress: MemoryFortress) -> None:
|
|
89
|
+
"""Multiple initializations don't break anything."""
|
|
90
|
+
c1 = fortress.initialize()
|
|
91
|
+
c2 = fortress.initialize()
|
|
92
|
+
assert c1.seal_algorithm == c2.seal_algorithm
|
|
93
|
+
|
|
94
|
+
def test_initialize_with_encryption(self, home: Path, seal_key: bytes) -> None:
|
|
95
|
+
"""Initialize with encryption enabled creates config."""
|
|
96
|
+
f = MemoryFortress(home, seal_key=seal_key, encryption_enabled=True)
|
|
97
|
+
config = f.initialize()
|
|
98
|
+
assert config.encryption_enabled is True
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# Sealing
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestSealing:
|
|
107
|
+
"""Tests for integrity seal operations."""
|
|
108
|
+
|
|
109
|
+
def test_seal_entry_adds_seal_field(
|
|
110
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry,
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Sealing adds the __fortress_seal field."""
|
|
113
|
+
data = fortress.seal_entry(sample_entry)
|
|
114
|
+
assert _SEAL_FIELD in data
|
|
115
|
+
assert _SEALED_AT_FIELD in data
|
|
116
|
+
|
|
117
|
+
def test_seal_is_deterministic(
|
|
118
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Same entry produces same seal (excluding timestamp)."""
|
|
121
|
+
data1 = fortress.seal_entry(sample_entry)
|
|
122
|
+
data2 = fortress.seal_entry(sample_entry)
|
|
123
|
+
# Seals match when the content is identical (timestamp excluded from seal)
|
|
124
|
+
# The seal covers the data dict which includes created_at etc.
|
|
125
|
+
assert data1[_SEAL_FIELD] == data2[_SEAL_FIELD]
|
|
126
|
+
|
|
127
|
+
def test_seal_changes_with_content(
|
|
128
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Different content produces different seal."""
|
|
131
|
+
data1 = fortress.seal_entry(sample_entry)
|
|
132
|
+
sample_entry.content = "Something completely different"
|
|
133
|
+
data2 = fortress.seal_entry(sample_entry)
|
|
134
|
+
assert data1[_SEAL_FIELD] != data2[_SEAL_FIELD]
|
|
135
|
+
|
|
136
|
+
def test_seal_is_hex_string(
|
|
137
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry,
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Seal is a 64-char hex string (SHA-256)."""
|
|
140
|
+
data = fortress.seal_entry(sample_entry)
|
|
141
|
+
seal = data[_SEAL_FIELD]
|
|
142
|
+
assert len(seal) == 64
|
|
143
|
+
assert all(c in "0123456789abcdef" for c in seal)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
# Verify and Load
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class TestVerifyAndLoad:
|
|
152
|
+
"""Tests for integrity verification on load."""
|
|
153
|
+
|
|
154
|
+
def test_verify_sealed_entry(
|
|
155
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Sealed entry passes verification."""
|
|
158
|
+
path = fortress.save_sealed(home, sample_entry)
|
|
159
|
+
entry, result = fortress.verify_and_load(path)
|
|
160
|
+
assert entry is not None
|
|
161
|
+
assert result.verified is True
|
|
162
|
+
assert result.tampered is False
|
|
163
|
+
assert entry.content == sample_entry.content
|
|
164
|
+
|
|
165
|
+
def test_detect_tampering(
|
|
166
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Modified content is detected as tampering."""
|
|
169
|
+
path = fortress.save_sealed(home, sample_entry)
|
|
170
|
+
|
|
171
|
+
# Tamper with the file
|
|
172
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
173
|
+
data["content"] = "I have been tampered with!"
|
|
174
|
+
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
175
|
+
|
|
176
|
+
entry, result = fortress.verify_and_load(path)
|
|
177
|
+
assert entry is None
|
|
178
|
+
assert result.tampered is True
|
|
179
|
+
assert result.verified is False
|
|
180
|
+
assert "tampering" in result.error.lower()
|
|
181
|
+
|
|
182
|
+
def test_detect_tag_tampering(
|
|
183
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Modified tags are detected as tampering."""
|
|
186
|
+
path = fortress.save_sealed(home, sample_entry)
|
|
187
|
+
|
|
188
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
189
|
+
data["tags"] = ["hacked"]
|
|
190
|
+
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
191
|
+
|
|
192
|
+
entry, result = fortress.verify_and_load(path)
|
|
193
|
+
assert result.tampered is True
|
|
194
|
+
|
|
195
|
+
def test_detect_importance_tampering(
|
|
196
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Modified importance score is detected."""
|
|
199
|
+
path = fortress.save_sealed(home, sample_entry)
|
|
200
|
+
|
|
201
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
202
|
+
data["importance"] = 1.0
|
|
203
|
+
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
204
|
+
|
|
205
|
+
entry, result = fortress.verify_and_load(path)
|
|
206
|
+
assert result.tampered is True
|
|
207
|
+
|
|
208
|
+
def test_legacy_unsealed_memory(self, fortress: MemoryFortress, home: Path) -> None:
|
|
209
|
+
"""Legacy memories without seals load with verified=None."""
|
|
210
|
+
entry = MemoryEntry(
|
|
211
|
+
memory_id="legacy123",
|
|
212
|
+
content="Old memory without seal",
|
|
213
|
+
tags=["legacy"],
|
|
214
|
+
source="test",
|
|
215
|
+
layer=MemoryLayer.SHORT_TERM,
|
|
216
|
+
)
|
|
217
|
+
path = home / "memory" / "short-term" / "legacy123.json"
|
|
218
|
+
path.write_text(entry.model_dump_json(indent=2), encoding="utf-8")
|
|
219
|
+
|
|
220
|
+
loaded, result = fortress.verify_and_load(path)
|
|
221
|
+
assert loaded is not None
|
|
222
|
+
assert result.sealed is False
|
|
223
|
+
assert result.verified is None
|
|
224
|
+
assert result.tampered is False
|
|
225
|
+
|
|
226
|
+
def test_corrupt_json_file(self, fortress: MemoryFortress, home: Path) -> None:
|
|
227
|
+
"""Corrupt JSON file returns error result."""
|
|
228
|
+
path = home / "memory" / "short-term" / "corrupt.json"
|
|
229
|
+
path.write_text("not valid json {{{", encoding="utf-8")
|
|
230
|
+
|
|
231
|
+
entry, result = fortress.verify_and_load(path)
|
|
232
|
+
assert entry is None
|
|
233
|
+
assert result.error is not None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# ---------------------------------------------------------------------------
|
|
237
|
+
# Save Sealed
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class TestSaveSealed:
|
|
242
|
+
"""Tests for atomic sealed writes."""
|
|
243
|
+
|
|
244
|
+
def test_save_creates_file(
|
|
245
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Save creates the sealed JSON file."""
|
|
248
|
+
path = fortress.save_sealed(home, sample_entry)
|
|
249
|
+
assert path.exists()
|
|
250
|
+
|
|
251
|
+
def test_saved_file_contains_seal(
|
|
252
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
|
|
253
|
+
) -> None:
|
|
254
|
+
"""Saved file contains the fortress seal."""
|
|
255
|
+
path = fortress.save_sealed(home, sample_entry)
|
|
256
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
257
|
+
assert _SEAL_FIELD in data
|
|
258
|
+
assert _SEALED_AT_FIELD in data
|
|
259
|
+
|
|
260
|
+
def test_roundtrip_save_load(
|
|
261
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
|
|
262
|
+
) -> None:
|
|
263
|
+
"""Save then load returns identical entry."""
|
|
264
|
+
path = fortress.save_sealed(home, sample_entry)
|
|
265
|
+
loaded, result = fortress.verify_and_load(path)
|
|
266
|
+
assert loaded is not None
|
|
267
|
+
assert loaded.memory_id == sample_entry.memory_id
|
|
268
|
+
assert loaded.content == sample_entry.content
|
|
269
|
+
assert loaded.tags == sample_entry.tags
|
|
270
|
+
|
|
271
|
+
def test_no_tmp_file_left(
|
|
272
|
+
self, fortress: MemoryFortress, sample_entry: MemoryEntry, home: Path,
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Atomic write leaves no .tmp file."""
|
|
275
|
+
path = fortress.save_sealed(home, sample_entry)
|
|
276
|
+
tmp = path.with_suffix(".json.tmp")
|
|
277
|
+
assert not tmp.exists()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# Verify All
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class TestVerifyAll:
|
|
286
|
+
"""Tests for full memory scan."""
|
|
287
|
+
|
|
288
|
+
def test_verify_all_empty(self, fortress: MemoryFortress, home: Path) -> None:
|
|
289
|
+
"""Verify all on empty memory returns empty."""
|
|
290
|
+
results = fortress.verify_all(home)
|
|
291
|
+
assert results == []
|
|
292
|
+
|
|
293
|
+
def test_verify_all_sealed(
|
|
294
|
+
self, fortress: MemoryFortress, home: Path,
|
|
295
|
+
) -> None:
|
|
296
|
+
"""Verify all with sealed memories passes."""
|
|
297
|
+
for i in range(3):
|
|
298
|
+
entry = MemoryEntry(
|
|
299
|
+
memory_id=f"mem{i:03d}",
|
|
300
|
+
content=f"Memory number {i}",
|
|
301
|
+
tags=["batch"],
|
|
302
|
+
source="test",
|
|
303
|
+
layer=MemoryLayer.SHORT_TERM,
|
|
304
|
+
)
|
|
305
|
+
fortress.save_sealed(home, entry)
|
|
306
|
+
|
|
307
|
+
results = fortress.verify_all(home)
|
|
308
|
+
assert len(results) == 3
|
|
309
|
+
assert all(r.verified is True for r in results)
|
|
310
|
+
|
|
311
|
+
def test_verify_all_detects_tampering(
|
|
312
|
+
self, fortress: MemoryFortress, home: Path,
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Verify all detects tampered memories in batch."""
|
|
315
|
+
for i in range(3):
|
|
316
|
+
entry = MemoryEntry(
|
|
317
|
+
memory_id=f"scan{i:03d}",
|
|
318
|
+
content=f"Memory {i}",
|
|
319
|
+
tags=["scan"],
|
|
320
|
+
source="test",
|
|
321
|
+
layer=MemoryLayer.SHORT_TERM,
|
|
322
|
+
)
|
|
323
|
+
fortress.save_sealed(home, entry)
|
|
324
|
+
|
|
325
|
+
# Tamper with one
|
|
326
|
+
tampered_path = home / "memory" / "short-term" / "scan001.json"
|
|
327
|
+
data = json.loads(tampered_path.read_text(encoding="utf-8"))
|
|
328
|
+
data["content"] = "HACKED"
|
|
329
|
+
tampered_path.write_text(json.dumps(data), encoding="utf-8")
|
|
330
|
+
|
|
331
|
+
results = fortress.verify_all(home)
|
|
332
|
+
tampered = [r for r in results if r.tampered]
|
|
333
|
+
verified = [r for r in results if r.verified]
|
|
334
|
+
assert len(tampered) == 1
|
|
335
|
+
assert len(verified) == 2
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# ---------------------------------------------------------------------------
|
|
339
|
+
# Seal Existing (Migration)
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class TestSealExisting:
|
|
344
|
+
"""Tests for migrating legacy memories."""
|
|
345
|
+
|
|
346
|
+
def test_seal_existing_memories(
|
|
347
|
+
self, fortress: MemoryFortress, home: Path,
|
|
348
|
+
) -> None:
|
|
349
|
+
"""Seal existing unsealed memories."""
|
|
350
|
+
for i in range(3):
|
|
351
|
+
entry = MemoryEntry(
|
|
352
|
+
memory_id=f"old{i:03d}",
|
|
353
|
+
content=f"Unsealed memory {i}",
|
|
354
|
+
tags=["legacy"],
|
|
355
|
+
source="test",
|
|
356
|
+
layer=MemoryLayer.MID_TERM,
|
|
357
|
+
)
|
|
358
|
+
path = home / "memory" / "mid-term" / f"old{i:03d}.json"
|
|
359
|
+
path.write_text(entry.model_dump_json(indent=2), encoding="utf-8")
|
|
360
|
+
|
|
361
|
+
sealed_count = fortress.seal_existing(home)
|
|
362
|
+
assert sealed_count == 3
|
|
363
|
+
|
|
364
|
+
# Verify they're now sealed
|
|
365
|
+
results = fortress.verify_all(home)
|
|
366
|
+
assert all(r.verified is True for r in results)
|
|
367
|
+
|
|
368
|
+
def test_seal_existing_skips_already_sealed(
|
|
369
|
+
self, fortress: MemoryFortress, home: Path,
|
|
370
|
+
) -> None:
|
|
371
|
+
"""Already-sealed memories are not re-sealed."""
|
|
372
|
+
entry = MemoryEntry(
|
|
373
|
+
memory_id="alreadysealed",
|
|
374
|
+
content="Already protected",
|
|
375
|
+
tags=["sealed"],
|
|
376
|
+
source="test",
|
|
377
|
+
layer=MemoryLayer.LONG_TERM,
|
|
378
|
+
)
|
|
379
|
+
fortress.save_sealed(home, entry)
|
|
380
|
+
|
|
381
|
+
sealed_count = fortress.seal_existing(home)
|
|
382
|
+
assert sealed_count == 0
|
|
383
|
+
|
|
384
|
+
def test_seal_existing_empty(
|
|
385
|
+
self, fortress: MemoryFortress, home: Path,
|
|
386
|
+
) -> None:
|
|
387
|
+
"""No memories to seal returns 0."""
|
|
388
|
+
assert fortress.seal_existing(home) == 0
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ---------------------------------------------------------------------------
|
|
392
|
+
# Encryption
|
|
393
|
+
# ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class TestEncryption:
|
|
397
|
+
"""Tests for at-rest encryption."""
|
|
398
|
+
|
|
399
|
+
def test_encrypted_roundtrip(self, home: Path) -> None:
|
|
400
|
+
"""Encrypt then decrypt returns original content."""
|
|
401
|
+
fortress = MemoryFortress(home, encryption_enabled=True)
|
|
402
|
+
fortress.initialize()
|
|
403
|
+
|
|
404
|
+
entry = MemoryEntry(
|
|
405
|
+
memory_id="encrypted01",
|
|
406
|
+
content="Top secret sovereign data",
|
|
407
|
+
tags=["encrypted"],
|
|
408
|
+
source="test",
|
|
409
|
+
layer=MemoryLayer.SHORT_TERM,
|
|
410
|
+
importance=0.9,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
path = fortress.save_sealed(home, entry)
|
|
414
|
+
loaded, result = fortress.verify_and_load(path)
|
|
415
|
+
assert loaded is not None
|
|
416
|
+
assert loaded.content == "Top secret sovereign data"
|
|
417
|
+
assert result.verified is True
|
|
418
|
+
|
|
419
|
+
def test_encrypted_content_not_plaintext(self, home: Path) -> None:
|
|
420
|
+
"""Encrypted file does not contain plaintext content."""
|
|
421
|
+
fortress = MemoryFortress(home, encryption_enabled=True)
|
|
422
|
+
fortress.initialize()
|
|
423
|
+
|
|
424
|
+
entry = MemoryEntry(
|
|
425
|
+
memory_id="hidden01",
|
|
426
|
+
content="This text should be encrypted on disk",
|
|
427
|
+
tags=["secret"],
|
|
428
|
+
source="test",
|
|
429
|
+
layer=MemoryLayer.SHORT_TERM,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
path = fortress.save_sealed(home, entry)
|
|
433
|
+
raw = path.read_text(encoding="utf-8")
|
|
434
|
+
assert "This text should be encrypted on disk" not in raw
|
|
435
|
+
assert _ENCRYPTED_FIELD in raw
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
# ---------------------------------------------------------------------------
|
|
439
|
+
# Different Seal Keys
|
|
440
|
+
# ---------------------------------------------------------------------------
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class TestSealKeys:
|
|
444
|
+
"""Tests for seal key behavior."""
|
|
445
|
+
|
|
446
|
+
def test_different_keys_different_seals(
|
|
447
|
+
self, home: Path, sample_entry: MemoryEntry,
|
|
448
|
+
) -> None:
|
|
449
|
+
"""Different seal keys produce different seals."""
|
|
450
|
+
f1 = MemoryFortress(home, seal_key=b"key-one-32-bytes-padded-here!!!!")
|
|
451
|
+
f1.initialize()
|
|
452
|
+
f2 = MemoryFortress(home, seal_key=b"key-two-32-bytes-padded-here!!!!")
|
|
453
|
+
f2.initialize()
|
|
454
|
+
|
|
455
|
+
data1 = f1.seal_entry(sample_entry)
|
|
456
|
+
data2 = f2.seal_entry(sample_entry)
|
|
457
|
+
assert data1[_SEAL_FIELD] != data2[_SEAL_FIELD]
|
|
458
|
+
|
|
459
|
+
def test_wrong_key_detects_tampering(
|
|
460
|
+
self, home: Path, sample_entry: MemoryEntry,
|
|
461
|
+
) -> None:
|
|
462
|
+
"""Loading with wrong seal key triggers tamper alert."""
|
|
463
|
+
f1 = MemoryFortress(home, seal_key=b"original-key-32-bytes-padded!!!")
|
|
464
|
+
f1.initialize()
|
|
465
|
+
path = f1.save_sealed(home, sample_entry)
|
|
466
|
+
|
|
467
|
+
f2 = MemoryFortress(home, seal_key=b"different-key-32-bytes-padded!!")
|
|
468
|
+
f2.initialize()
|
|
469
|
+
_, result = f2.verify_and_load(path)
|
|
470
|
+
assert result.tampered is True
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
# ---------------------------------------------------------------------------
|
|
474
|
+
# Status
|
|
475
|
+
# ---------------------------------------------------------------------------
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
class TestStatus:
|
|
479
|
+
"""Tests for fortress status reporting."""
|
|
480
|
+
|
|
481
|
+
def test_status_returns_config(self, fortress: MemoryFortress) -> None:
|
|
482
|
+
"""Status returns current config info."""
|
|
483
|
+
status = fortress.status()
|
|
484
|
+
assert status["enabled"] is True
|
|
485
|
+
assert status["seal_algorithm"] == "hmac-sha256"
|
|
486
|
+
assert status["has_seal_key"] is True
|
|
487
|
+
|
|
488
|
+
def test_status_reflects_encryption(self, home: Path) -> None:
|
|
489
|
+
"""Status reflects encryption setting."""
|
|
490
|
+
f = MemoryFortress(home, seal_key=b"k" * 32, encryption_enabled=True)
|
|
491
|
+
f.initialize()
|
|
492
|
+
status = f.status()
|
|
493
|
+
assert status["encryption_enabled"] is True
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# ---------------------------------------------------------------------------
|
|
497
|
+
# Audit Trail
|
|
498
|
+
# ---------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class TestAuditTrail:
|
|
502
|
+
"""Tests for security audit integration."""
|
|
503
|
+
|
|
504
|
+
def test_init_writes_audit(
|
|
505
|
+
self, home: Path, seal_key: bytes,
|
|
506
|
+
) -> None:
|
|
507
|
+
"""Initialization writes an audit event."""
|
|
508
|
+
f = MemoryFortress(home, seal_key=seal_key)
|
|
509
|
+
f.initialize()
|
|
510
|
+
|
|
511
|
+
audit_log = home / "security" / "audit.log"
|
|
512
|
+
assert audit_log.exists()
|
|
513
|
+
content = audit_log.read_text(encoding="utf-8")
|
|
514
|
+
assert "FORTRESS_INIT" in content
|
|
515
|
+
|
|
516
|
+
def test_tamper_writes_audit(
|
|
517
|
+
self, fortress: MemoryFortress, home: Path,
|
|
518
|
+
) -> None:
|
|
519
|
+
"""Tamper detection writes audit event."""
|
|
520
|
+
entry = MemoryEntry(
|
|
521
|
+
memory_id="audittamp",
|
|
522
|
+
content="Watch me",
|
|
523
|
+
tags=["audit"],
|
|
524
|
+
source="test",
|
|
525
|
+
layer=MemoryLayer.SHORT_TERM,
|
|
526
|
+
)
|
|
527
|
+
path = fortress.save_sealed(home, entry)
|
|
528
|
+
|
|
529
|
+
# Tamper
|
|
530
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
531
|
+
data["content"] = "EVIL"
|
|
532
|
+
path.write_text(json.dumps(data), encoding="utf-8")
|
|
533
|
+
|
|
534
|
+
fortress.verify_and_load(path)
|
|
535
|
+
|
|
536
|
+
audit_log = home / "security" / "audit.log"
|
|
537
|
+
content = audit_log.read_text(encoding="utf-8")
|
|
538
|
+
assert "MEMORY_TAMPER_ALERT" in content
|
|
539
|
+
|
|
540
|
+
def test_scan_writes_audit(
|
|
541
|
+
self, fortress: MemoryFortress, home: Path,
|
|
542
|
+
) -> None:
|
|
543
|
+
"""Full scan writes audit event."""
|
|
544
|
+
fortress.verify_all(home)
|
|
545
|
+
|
|
546
|
+
audit_log = home / "security" / "audit.log"
|
|
547
|
+
content = audit_log.read_text(encoding="utf-8")
|
|
548
|
+
assert "FORTRESS_SCAN" in content
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# ---------------------------------------------------------------------------
|
|
552
|
+
# Model tests
|
|
553
|
+
# ---------------------------------------------------------------------------
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class TestModels:
|
|
557
|
+
"""Tests for fortress models."""
|
|
558
|
+
|
|
559
|
+
def test_fortress_config_defaults(self) -> None:
|
|
560
|
+
"""FortressConfig has sensible defaults."""
|
|
561
|
+
config = FortressConfig()
|
|
562
|
+
assert config.enabled is True
|
|
563
|
+
assert config.encryption_enabled is False
|
|
564
|
+
assert config.seal_algorithm == "hmac-sha256"
|
|
565
|
+
|
|
566
|
+
def test_seal_result_defaults(self) -> None:
|
|
567
|
+
"""SealResult has sensible defaults."""
|
|
568
|
+
result = SealResult(memory_id="test", sealed=True)
|
|
569
|
+
assert result.tampered is False
|
|
570
|
+
assert result.error is None
|
|
571
|
+
assert result.verified is None
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Unit tests for the memory pillar module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from skcapstone.models import MemoryLayer, PillarStatus
|
|
12
|
+
from skcapstone.pillars.memory import get_memory_stats, initialize_memory
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestInitializeMemory:
|
|
16
|
+
"""Tests for initialize_memory()."""
|
|
17
|
+
|
|
18
|
+
def test_creates_memory_directory(self, tmp_agent_home: Path):
|
|
19
|
+
initialize_memory(tmp_agent_home)
|
|
20
|
+
assert (tmp_agent_home / "memory").is_dir()
|
|
21
|
+
|
|
22
|
+
def test_creates_layer_subdirectories(self, tmp_agent_home: Path):
|
|
23
|
+
initialize_memory(tmp_agent_home)
|
|
24
|
+
memory_dir = tmp_agent_home / "memory"
|
|
25
|
+
for layer in MemoryLayer:
|
|
26
|
+
assert (memory_dir / layer.value).is_dir(), f"missing layer: {layer.value}"
|
|
27
|
+
|
|
28
|
+
def test_creates_short_term_dir(self, tmp_agent_home: Path):
|
|
29
|
+
initialize_memory(tmp_agent_home)
|
|
30
|
+
assert (tmp_agent_home / "memory" / "short-term").is_dir()
|
|
31
|
+
|
|
32
|
+
def test_creates_mid_term_dir(self, tmp_agent_home: Path):
|
|
33
|
+
initialize_memory(tmp_agent_home)
|
|
34
|
+
assert (tmp_agent_home / "memory" / "mid-term").is_dir()
|
|
35
|
+
|
|
36
|
+
def test_creates_long_term_dir(self, tmp_agent_home: Path):
|
|
37
|
+
initialize_memory(tmp_agent_home)
|
|
38
|
+
assert (tmp_agent_home / "memory" / "long-term").is_dir()
|
|
39
|
+
|
|
40
|
+
def test_returns_active_status(self, tmp_agent_home: Path):
|
|
41
|
+
state = initialize_memory(tmp_agent_home)
|
|
42
|
+
assert state.status == PillarStatus.ACTIVE
|
|
43
|
+
|
|
44
|
+
def test_store_path_set_to_memory_dir(self, tmp_agent_home: Path):
|
|
45
|
+
state = initialize_memory(tmp_agent_home)
|
|
46
|
+
assert state.store_path == tmp_agent_home / "memory"
|
|
47
|
+
|
|
48
|
+
def test_idempotent_second_call(self, tmp_agent_home: Path):
|
|
49
|
+
initialize_memory(tmp_agent_home)
|
|
50
|
+
state2 = initialize_memory(tmp_agent_home)
|
|
51
|
+
assert state2.status == PillarStatus.ACTIVE
|
|
52
|
+
|
|
53
|
+
def test_memory_home_param_ignored(self, tmp_agent_home: Path):
|
|
54
|
+
"""memory_home is kept for backward compatibility but unused."""
|
|
55
|
+
state = initialize_memory(tmp_agent_home, memory_home=Path("/dev/null"))
|
|
56
|
+
assert state.store_path == tmp_agent_home / "memory"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestGetMemoryStats:
|
|
60
|
+
"""Tests for get_memory_stats()."""
|
|
61
|
+
|
|
62
|
+
def test_returns_zero_counts_when_empty(self, tmp_agent_home: Path):
|
|
63
|
+
initialize_memory(tmp_agent_home)
|
|
64
|
+
stats = get_memory_stats(tmp_agent_home)
|
|
65
|
+
assert stats["short_term"] == 0
|
|
66
|
+
assert stats["mid_term"] == 0
|
|
67
|
+
assert stats["long_term"] == 0
|
|
68
|
+
assert stats["total"] == 0
|
|
69
|
+
|
|
70
|
+
def test_counts_json_files_in_short_term(self, tmp_agent_home: Path):
|
|
71
|
+
initialize_memory(tmp_agent_home)
|
|
72
|
+
short_dir = tmp_agent_home / "memory" / "short-term"
|
|
73
|
+
(short_dir / "mem001.json").write_text("{}", encoding="utf-8")
|
|
74
|
+
(short_dir / "mem002.json").write_text("{}", encoding="utf-8")
|
|
75
|
+
|
|
76
|
+
stats = get_memory_stats(tmp_agent_home)
|
|
77
|
+
assert stats["short_term"] == 2
|
|
78
|
+
assert stats["total"] == 2
|
|
79
|
+
|
|
80
|
+
def test_counts_json_files_in_mid_term(self, tmp_agent_home: Path):
|
|
81
|
+
initialize_memory(tmp_agent_home)
|
|
82
|
+
mid_dir = tmp_agent_home / "memory" / "mid-term"
|
|
83
|
+
(mid_dir / "mem001.json").write_text("{}", encoding="utf-8")
|
|
84
|
+
|
|
85
|
+
stats = get_memory_stats(tmp_agent_home)
|
|
86
|
+
assert stats["mid_term"] == 1
|
|
87
|
+
|
|
88
|
+
def test_counts_json_files_in_long_term(self, tmp_agent_home: Path):
|
|
89
|
+
initialize_memory(tmp_agent_home)
|
|
90
|
+
long_dir = tmp_agent_home / "memory" / "long-term"
|
|
91
|
+
(long_dir / "deep001.json").write_text("{}", encoding="utf-8")
|
|
92
|
+
(long_dir / "deep002.json").write_text("{}", encoding="utf-8")
|
|
93
|
+
(long_dir / "deep003.json").write_text("{}", encoding="utf-8")
|
|
94
|
+
|
|
95
|
+
stats = get_memory_stats(tmp_agent_home)
|
|
96
|
+
assert stats["long_term"] == 3
|
|
97
|
+
|
|
98
|
+
def test_total_is_sum_of_all_layers(self, tmp_agent_home: Path):
|
|
99
|
+
initialize_memory(tmp_agent_home)
|
|
100
|
+
(tmp_agent_home / "memory" / "short-term" / "a.json").write_text("{}")
|
|
101
|
+
(tmp_agent_home / "memory" / "mid-term" / "b.json").write_text("{}")
|
|
102
|
+
(tmp_agent_home / "memory" / "long-term" / "c.json").write_text("{}")
|
|
103
|
+
|
|
104
|
+
stats = get_memory_stats(tmp_agent_home)
|
|
105
|
+
assert stats["total"] == 3
|
|
106
|
+
|
|
107
|
+
def test_non_json_files_not_counted(self, tmp_agent_home: Path):
|
|
108
|
+
initialize_memory(tmp_agent_home)
|
|
109
|
+
short_dir = tmp_agent_home / "memory" / "short-term"
|
|
110
|
+
(short_dir / "note.md").write_text("# note")
|
|
111
|
+
(short_dir / "real.json").write_text("{}")
|
|
112
|
+
|
|
113
|
+
stats = get_memory_stats(tmp_agent_home)
|
|
114
|
+
assert stats["short_term"] == 1
|
|
115
|
+
|
|
116
|
+
def test_returns_dict_with_expected_keys(self, tmp_agent_home: Path):
|
|
117
|
+
initialize_memory(tmp_agent_home)
|
|
118
|
+
stats = get_memory_stats(tmp_agent_home)
|
|
119
|
+
assert set(stats.keys()) == {"short_term", "mid_term", "long_term", "total"}
|