@smilintux/skcapstone 0.1.0 → 0.2.4
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 +880 -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 +191 -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 +398 -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 +357 -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 +264 -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,942 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SKSecurity KMS — Sovereign Key Management Service.
|
|
3
|
+
|
|
4
|
+
Wraps sksecurity.kms.KMS for cryptographic operations while providing
|
|
5
|
+
agent-specific features: service key derivation, team member ACLs,
|
|
6
|
+
label-based lookup, and key expiry management.
|
|
7
|
+
|
|
8
|
+
Every operation is logged to the security audit trail.
|
|
9
|
+
|
|
10
|
+
Key hierarchy:
|
|
11
|
+
Agent identity key (PGP, managed by CapAuth)
|
|
12
|
+
└── Master KMS key (derived via HKDF from identity fingerprint)
|
|
13
|
+
├── Service keys (per-service HKDF derivation)
|
|
14
|
+
├── Team keys (shared keys with member ACL)
|
|
15
|
+
└── Subkeys (delegatable, revocable)
|
|
16
|
+
|
|
17
|
+
Crypto backend: sksecurity.kms — AES-256-GCM key wrapping,
|
|
18
|
+
HKDF-SHA256 derivation, scrypt master key sealing.
|
|
19
|
+
|
|
20
|
+
Storage layout:
|
|
21
|
+
~/.skcapstone/security/kms/
|
|
22
|
+
├── keystore.json # Key metadata (KeyRecord list)
|
|
23
|
+
├── keys/ # Encrypted key material
|
|
24
|
+
│ └── <key_id>.key.enc # AES-256-GCM encrypted raw key bytes
|
|
25
|
+
├── rotation-log.json # Key rotation history
|
|
26
|
+
└── backend/ # sksecurity KMS 4-tier hierarchy
|
|
27
|
+
├── keys/ # Wrapped keys (master→team→agent→DEK)
|
|
28
|
+
└── audit.log # Backend audit trail
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
store = KeyStore(home)
|
|
32
|
+
key = store.derive_service_key("api-gateway")
|
|
33
|
+
team_key = store.create_team_key("dev-team", members=["opus", "lumina"])
|
|
34
|
+
store.rotate_key(key.key_id)
|
|
35
|
+
|
|
36
|
+
# Access the full sksecurity 4-tier KMS backend:
|
|
37
|
+
backend = store.backend
|
|
38
|
+
backend.create_team_key("deployment-alpha")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import hashlib
|
|
44
|
+
import json
|
|
45
|
+
import logging
|
|
46
|
+
import os
|
|
47
|
+
import secrets
|
|
48
|
+
from datetime import datetime, timedelta, timezone
|
|
49
|
+
from enum import Enum
|
|
50
|
+
from pathlib import Path
|
|
51
|
+
from typing import Any, Optional
|
|
52
|
+
|
|
53
|
+
from pydantic import BaseModel, Field
|
|
54
|
+
|
|
55
|
+
logger = logging.getLogger("skcapstone.kms")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
# sksecurity backend integration
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
from sksecurity.kms import (
|
|
64
|
+
KMS as BackendKMS,
|
|
65
|
+
FileKeyStore as BackendFileKeyStore,
|
|
66
|
+
_hkdf_derive as _backend_hkdf,
|
|
67
|
+
_aes_gcm_encrypt as _backend_encrypt,
|
|
68
|
+
_aes_gcm_decrypt as _backend_decrypt,
|
|
69
|
+
)
|
|
70
|
+
_HAS_BACKEND = True
|
|
71
|
+
except ImportError:
|
|
72
|
+
_HAS_BACKEND = False
|
|
73
|
+
BackendKMS = None # type: ignore[assignment,misc]
|
|
74
|
+
BackendFileKeyStore = None # type: ignore[assignment,misc]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Models
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
class KeyType(str, Enum):
|
|
82
|
+
"""Types of managed keys."""
|
|
83
|
+
|
|
84
|
+
MASTER = "master"
|
|
85
|
+
SERVICE = "service"
|
|
86
|
+
TEAM = "team"
|
|
87
|
+
SUBKEY = "subkey"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class KeyStatus(str, Enum):
|
|
91
|
+
"""Lifecycle status of a key."""
|
|
92
|
+
|
|
93
|
+
ACTIVE = "active"
|
|
94
|
+
ROTATED = "rotated"
|
|
95
|
+
REVOKED = "revoked"
|
|
96
|
+
EXPIRED = "expired"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class KeyRecord(BaseModel):
|
|
100
|
+
"""Metadata for a managed key (the actual key material is stored separately)."""
|
|
101
|
+
|
|
102
|
+
key_id: str = Field(description="Unique key identifier (SHA-256 hash)")
|
|
103
|
+
key_type: KeyType
|
|
104
|
+
algorithm: str = Field(default="HKDF-SHA256+AES-256-GCM")
|
|
105
|
+
label: str = Field(description="Human-readable label (e.g., 'api-gateway', 'dev-team')")
|
|
106
|
+
parent_key_id: Optional[str] = Field(default=None, description="Parent key for derivations")
|
|
107
|
+
fingerprint: str = Field(description="SHA-256 of the raw key material")
|
|
108
|
+
status: KeyStatus = KeyStatus.ACTIVE
|
|
109
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
110
|
+
rotated_at: Optional[datetime] = None
|
|
111
|
+
expires_at: Optional[datetime] = None
|
|
112
|
+
version: int = Field(default=1, description="Key version (incremented on rotation)")
|
|
113
|
+
members: list[str] = Field(default_factory=list, description="Team key members (agent names)")
|
|
114
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class RotationEntry(BaseModel):
|
|
118
|
+
"""Audit record for a key rotation event."""
|
|
119
|
+
|
|
120
|
+
key_id: str
|
|
121
|
+
old_fingerprint: str
|
|
122
|
+
new_fingerprint: str
|
|
123
|
+
old_version: int
|
|
124
|
+
new_version: int
|
|
125
|
+
rotated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
126
|
+
reason: str = ""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Cryptographic helpers — delegates to sksecurity when available
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
def _derive_key(master_material: bytes, info: bytes, length: int = 32) -> bytes:
|
|
134
|
+
"""Derive a key using HKDF-SHA256.
|
|
135
|
+
|
|
136
|
+
Delegates to sksecurity.kms._hkdf_derive when available, otherwise
|
|
137
|
+
uses the cryptography library directly.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
master_material: Input keying material.
|
|
141
|
+
info: Context and application-specific info string.
|
|
142
|
+
length: Desired output key length in bytes.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Derived key bytes.
|
|
146
|
+
"""
|
|
147
|
+
if _HAS_BACKEND:
|
|
148
|
+
info_str = info.decode("utf-8") if isinstance(info, bytes) else info
|
|
149
|
+
return _backend_hkdf(master_material, info_str, length)
|
|
150
|
+
|
|
151
|
+
from cryptography.hazmat.primitives.hashes import SHA256
|
|
152
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
153
|
+
|
|
154
|
+
hkdf = HKDF(
|
|
155
|
+
algorithm=SHA256(),
|
|
156
|
+
length=length,
|
|
157
|
+
salt=None,
|
|
158
|
+
info=info,
|
|
159
|
+
)
|
|
160
|
+
return hkdf.derive(master_material)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _encrypt_at_rest(data: bytes, key_material: bytes) -> bytes:
|
|
164
|
+
"""Encrypt data for at-rest storage using AES-256-GCM.
|
|
165
|
+
|
|
166
|
+
Returns nonce (12 bytes) || ciphertext || tag (16 bytes).
|
|
167
|
+
Delegates to sksecurity.kms._aes_gcm_encrypt when available.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
data: Plaintext bytes.
|
|
171
|
+
key_material: 32-byte AES key.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Ciphertext bytes (nonce || ciphertext || tag).
|
|
175
|
+
"""
|
|
176
|
+
if _HAS_BACKEND:
|
|
177
|
+
return _backend_encrypt(key_material, data)
|
|
178
|
+
|
|
179
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
180
|
+
|
|
181
|
+
nonce = os.urandom(12)
|
|
182
|
+
aesgcm = AESGCM(key_material[:32])
|
|
183
|
+
ct = aesgcm.encrypt(nonce, data, None)
|
|
184
|
+
return nonce + ct
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _decrypt_at_rest(token: bytes, key_material: bytes) -> bytes:
|
|
188
|
+
"""Decrypt data from at-rest AES-256-GCM storage.
|
|
189
|
+
|
|
190
|
+
Expects nonce (12 bytes) || ciphertext || tag (16 bytes).
|
|
191
|
+
Delegates to sksecurity.kms._aes_gcm_decrypt when available.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
token: Ciphertext bytes (nonce || ciphertext || tag).
|
|
195
|
+
key_material: 32-byte AES key.
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Plaintext bytes.
|
|
199
|
+
"""
|
|
200
|
+
if _HAS_BACKEND:
|
|
201
|
+
return _backend_decrypt(key_material, token)
|
|
202
|
+
|
|
203
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
204
|
+
|
|
205
|
+
nonce, ct = token[:12], token[12:]
|
|
206
|
+
aesgcm = AESGCM(key_material[:32])
|
|
207
|
+
return aesgcm.decrypt(nonce, ct, None)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _fernet_encrypt(data: bytes, key: bytes) -> bytes:
|
|
211
|
+
"""Encrypt data using Fernet (AES-128-CBC + HMAC-SHA256).
|
|
212
|
+
|
|
213
|
+
Derives a 32-byte Fernet key from the provided key material using SHA-256,
|
|
214
|
+
then encodes it as URL-safe base64 as Fernet requires.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
data: Plaintext bytes.
|
|
218
|
+
key: Raw key material (any length).
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Fernet token bytes.
|
|
222
|
+
"""
|
|
223
|
+
import base64
|
|
224
|
+
|
|
225
|
+
from cryptography.fernet import Fernet
|
|
226
|
+
|
|
227
|
+
key32 = hashlib.sha256(key).digest()
|
|
228
|
+
fernet_key = base64.urlsafe_b64encode(key32)
|
|
229
|
+
return Fernet(fernet_key).encrypt(data)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _fernet_decrypt(token: bytes, key: bytes) -> bytes:
|
|
233
|
+
"""Decrypt a Fernet token.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
token: Fernet token bytes produced by :func:`_fernet_encrypt`.
|
|
237
|
+
key: Raw key material (any length) — must match the key used to encrypt.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Decrypted plaintext bytes.
|
|
241
|
+
"""
|
|
242
|
+
import base64
|
|
243
|
+
|
|
244
|
+
from cryptography.fernet import Fernet
|
|
245
|
+
|
|
246
|
+
key32 = hashlib.sha256(key).digest()
|
|
247
|
+
fernet_key = base64.urlsafe_b64encode(key32)
|
|
248
|
+
return Fernet(fernet_key).decrypt(token)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _key_fingerprint(raw: bytes) -> str:
|
|
252
|
+
"""Compute SHA-256 fingerprint of raw key material."""
|
|
253
|
+
return hashlib.sha256(raw).hexdigest()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _key_id(label: str, key_type: KeyType, version: int = 1) -> str:
|
|
257
|
+
"""Deterministic key ID from label + type + version."""
|
|
258
|
+
data = f"skcapstone:kms:{key_type.value}:{label}:v{version}"
|
|
259
|
+
return hashlib.sha256(data.encode()).hexdigest()[:16]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
# KeyStore
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
class KeyStore:
|
|
267
|
+
"""Sovereign key management store.
|
|
268
|
+
|
|
269
|
+
Wraps sksecurity.kms.KMS for cryptographic operations while
|
|
270
|
+
providing agent-specific key lifecycle management: derivation,
|
|
271
|
+
storage, rotation, team membership, and revocation.
|
|
272
|
+
|
|
273
|
+
When sksecurity is installed, the full 4-tier key hierarchy
|
|
274
|
+
(master -> team -> agent -> DEK) is available via the ``backend``
|
|
275
|
+
property. The agent-level API (service keys, team ACLs, subkeys)
|
|
276
|
+
is always available regardless of sksecurity availability.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
home: Agent home directory (~/.skcapstone).
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def __init__(self, home: Path) -> None:
|
|
283
|
+
self._home = home
|
|
284
|
+
self._kms_dir = home / "security" / "kms"
|
|
285
|
+
self._keys_dir = self._kms_dir / "keys"
|
|
286
|
+
self._keystore_file = self._kms_dir / "keystore.json"
|
|
287
|
+
self._rotation_log = self._kms_dir / "rotation-log.json"
|
|
288
|
+
self._master_material: Optional[bytes] = None
|
|
289
|
+
self._backend_kms: Optional[Any] = None
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def backend(self) -> Optional[Any]:
|
|
293
|
+
"""Access the sksecurity KMS backend for 4-tier operations.
|
|
294
|
+
|
|
295
|
+
Returns the underlying sksecurity.kms.KMS instance (unsealed)
|
|
296
|
+
for direct team/agent/DEK key management. Returns None if
|
|
297
|
+
sksecurity is not installed or backend initialization failed.
|
|
298
|
+
"""
|
|
299
|
+
return self._backend_kms
|
|
300
|
+
|
|
301
|
+
def initialize(self) -> KeyRecord:
|
|
302
|
+
"""Initialize the KMS and derive the master key.
|
|
303
|
+
|
|
304
|
+
The master key is derived from the agent's identity fingerprint
|
|
305
|
+
via HKDF. If no identity exists, a random master is generated.
|
|
306
|
+
When sksecurity is available, also initializes the 4-tier
|
|
307
|
+
backend KMS.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
KeyRecord for the master key.
|
|
311
|
+
"""
|
|
312
|
+
self._kms_dir.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
self._keys_dir.mkdir(exist_ok=True)
|
|
314
|
+
|
|
315
|
+
existing = self._load_records()
|
|
316
|
+
master = next((r for r in existing if r.key_type == KeyType.MASTER), None)
|
|
317
|
+
if master and master.status == KeyStatus.ACTIVE:
|
|
318
|
+
self._master_material = self._load_key_material(master.key_id)
|
|
319
|
+
self._init_backend()
|
|
320
|
+
return master
|
|
321
|
+
|
|
322
|
+
identity_material = self._get_identity_material()
|
|
323
|
+
raw_master = _derive_key(identity_material, b"skcapstone:kms:master", length=32)
|
|
324
|
+
self._master_material = raw_master
|
|
325
|
+
|
|
326
|
+
record = KeyRecord(
|
|
327
|
+
key_id=_key_id("master", KeyType.MASTER),
|
|
328
|
+
key_type=KeyType.MASTER,
|
|
329
|
+
label="master",
|
|
330
|
+
fingerprint=_key_fingerprint(raw_master),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
self._save_key_material(record.key_id, raw_master)
|
|
334
|
+
self._append_record(record)
|
|
335
|
+
self._init_backend()
|
|
336
|
+
self._audit("KMS_INIT", f"KMS initialized, master key {record.key_id}")
|
|
337
|
+
|
|
338
|
+
return record
|
|
339
|
+
|
|
340
|
+
def derive_service_key(
|
|
341
|
+
self,
|
|
342
|
+
service_name: str,
|
|
343
|
+
ttl_days: Optional[int] = None,
|
|
344
|
+
) -> KeyRecord:
|
|
345
|
+
"""Derive a service-specific key from the master.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
service_name: Service identifier (e.g., 'api-gateway', 'skchat').
|
|
349
|
+
ttl_days: Optional key expiry in days.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
KeyRecord for the new service key.
|
|
353
|
+
"""
|
|
354
|
+
master = self._ensure_master()
|
|
355
|
+
|
|
356
|
+
existing = self.get_key(service_name, KeyType.SERVICE)
|
|
357
|
+
if existing and existing.status == KeyStatus.ACTIVE:
|
|
358
|
+
return existing
|
|
359
|
+
|
|
360
|
+
info = f"skcapstone:kms:service:{service_name}".encode()
|
|
361
|
+
raw = _derive_key(self._master_material, info, length=32)
|
|
362
|
+
|
|
363
|
+
expires = None
|
|
364
|
+
if ttl_days:
|
|
365
|
+
expires = datetime.now(timezone.utc) + timedelta(days=ttl_days)
|
|
366
|
+
|
|
367
|
+
record = KeyRecord(
|
|
368
|
+
key_id=_key_id(service_name, KeyType.SERVICE),
|
|
369
|
+
key_type=KeyType.SERVICE,
|
|
370
|
+
label=service_name,
|
|
371
|
+
parent_key_id=master.key_id,
|
|
372
|
+
fingerprint=_key_fingerprint(raw),
|
|
373
|
+
expires_at=expires,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
self._save_key_material(record.key_id, raw)
|
|
377
|
+
self._append_record(record)
|
|
378
|
+
self._audit(
|
|
379
|
+
"KEY_DERIVE",
|
|
380
|
+
f"Derived service key '{service_name}' ({record.key_id})",
|
|
381
|
+
metadata={"key_type": "service", "label": service_name},
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return record
|
|
385
|
+
|
|
386
|
+
def derive_subkey(
|
|
387
|
+
self,
|
|
388
|
+
label: str,
|
|
389
|
+
parent_label: Optional[str] = None,
|
|
390
|
+
) -> KeyRecord:
|
|
391
|
+
"""Derive a subkey for delegation.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
label: Subkey label.
|
|
395
|
+
parent_label: Parent service key label (defaults to master).
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
KeyRecord for the new subkey.
|
|
399
|
+
"""
|
|
400
|
+
if parent_label:
|
|
401
|
+
parent = self.get_key(parent_label)
|
|
402
|
+
if not parent:
|
|
403
|
+
raise ValueError(f"Parent key '{parent_label}' not found")
|
|
404
|
+
parent_material = self._load_key_material(parent.key_id)
|
|
405
|
+
parent_id = parent.key_id
|
|
406
|
+
else:
|
|
407
|
+
self._ensure_master()
|
|
408
|
+
parent_material = self._master_material
|
|
409
|
+
parent_id = _key_id("master", KeyType.MASTER)
|
|
410
|
+
|
|
411
|
+
info = f"skcapstone:kms:subkey:{label}".encode()
|
|
412
|
+
raw = _derive_key(parent_material, info, length=32)
|
|
413
|
+
|
|
414
|
+
record = KeyRecord(
|
|
415
|
+
key_id=_key_id(label, KeyType.SUBKEY),
|
|
416
|
+
key_type=KeyType.SUBKEY,
|
|
417
|
+
label=label,
|
|
418
|
+
parent_key_id=parent_id,
|
|
419
|
+
fingerprint=_key_fingerprint(raw),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
self._save_key_material(record.key_id, raw)
|
|
423
|
+
self._append_record(record)
|
|
424
|
+
self._audit(
|
|
425
|
+
"KEY_DERIVE",
|
|
426
|
+
f"Derived subkey '{label}' ({record.key_id})",
|
|
427
|
+
metadata={"key_type": "subkey", "parent": parent_id},
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
return record
|
|
431
|
+
|
|
432
|
+
def create_team_key(
|
|
433
|
+
self,
|
|
434
|
+
team_name: str,
|
|
435
|
+
members: Optional[list[str]] = None,
|
|
436
|
+
) -> KeyRecord:
|
|
437
|
+
"""Create a shared team key.
|
|
438
|
+
|
|
439
|
+
Team keys are random (not derived from master) so they can be
|
|
440
|
+
independently rotated without affecting the key hierarchy.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
team_name: Team identifier.
|
|
444
|
+
members: Initial list of agent names with access.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
KeyRecord for the new team key.
|
|
448
|
+
"""
|
|
449
|
+
self._ensure_master()
|
|
450
|
+
|
|
451
|
+
existing = self.get_key(team_name, KeyType.TEAM)
|
|
452
|
+
if existing and existing.status == KeyStatus.ACTIVE:
|
|
453
|
+
return existing
|
|
454
|
+
|
|
455
|
+
raw = secrets.token_bytes(32)
|
|
456
|
+
|
|
457
|
+
record = KeyRecord(
|
|
458
|
+
key_id=_key_id(team_name, KeyType.TEAM),
|
|
459
|
+
key_type=KeyType.TEAM,
|
|
460
|
+
label=team_name,
|
|
461
|
+
fingerprint=_key_fingerprint(raw),
|
|
462
|
+
members=members or [],
|
|
463
|
+
algorithm="random+AES-256-GCM",
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
self._save_key_material(record.key_id, raw)
|
|
467
|
+
self._append_record(record)
|
|
468
|
+
self._audit(
|
|
469
|
+
"TEAM_KEY_CREATE",
|
|
470
|
+
f"Created team key '{team_name}' ({record.key_id}) with {len(record.members)} members",
|
|
471
|
+
metadata={"team": team_name, "members": record.members},
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
return record
|
|
475
|
+
|
|
476
|
+
def add_team_member(self, team_name: str, agent_name: str) -> KeyRecord:
|
|
477
|
+
"""Add a member to a team key's ACL.
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
team_name: Team key label.
|
|
481
|
+
agent_name: Agent name to add.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
Updated KeyRecord.
|
|
485
|
+
|
|
486
|
+
Raises:
|
|
487
|
+
ValueError: If team key not found.
|
|
488
|
+
"""
|
|
489
|
+
record = self.get_key(team_name, KeyType.TEAM)
|
|
490
|
+
if not record:
|
|
491
|
+
raise ValueError(f"Team key '{team_name}' not found")
|
|
492
|
+
|
|
493
|
+
if agent_name in record.members:
|
|
494
|
+
return record
|
|
495
|
+
|
|
496
|
+
record.members.append(agent_name)
|
|
497
|
+
self._update_record(record)
|
|
498
|
+
self._audit(
|
|
499
|
+
"TEAM_MEMBER_ADD",
|
|
500
|
+
f"Added '{agent_name}' to team '{team_name}'",
|
|
501
|
+
metadata={"team": team_name, "agent": agent_name},
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
return record
|
|
505
|
+
|
|
506
|
+
def remove_team_member(self, team_name: str, agent_name: str) -> KeyRecord:
|
|
507
|
+
"""Remove a member from a team key's ACL.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
team_name: Team key label.
|
|
511
|
+
agent_name: Agent name to remove.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
Updated KeyRecord.
|
|
515
|
+
|
|
516
|
+
Raises:
|
|
517
|
+
ValueError: If team key not found.
|
|
518
|
+
"""
|
|
519
|
+
record = self.get_key(team_name, KeyType.TEAM)
|
|
520
|
+
if not record:
|
|
521
|
+
raise ValueError(f"Team key '{team_name}' not found")
|
|
522
|
+
|
|
523
|
+
if agent_name not in record.members:
|
|
524
|
+
return record
|
|
525
|
+
|
|
526
|
+
record.members.remove(agent_name)
|
|
527
|
+
self._update_record(record)
|
|
528
|
+
self._audit(
|
|
529
|
+
"TEAM_MEMBER_REMOVE",
|
|
530
|
+
f"Removed '{agent_name}' from team '{team_name}'",
|
|
531
|
+
metadata={"team": team_name, "agent": agent_name},
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
return record
|
|
535
|
+
|
|
536
|
+
def rotate_key(self, key_id: str, reason: str = "") -> KeyRecord:
|
|
537
|
+
"""Rotate a key — generate new material, increment version.
|
|
538
|
+
|
|
539
|
+
The old key is marked ROTATED and a new active key replaces it.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
key_id: Key to rotate.
|
|
543
|
+
reason: Optional reason for the rotation.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
New KeyRecord for the rotated key.
|
|
547
|
+
|
|
548
|
+
Raises:
|
|
549
|
+
ValueError: If key not found.
|
|
550
|
+
"""
|
|
551
|
+
old = self._get_record_by_id(key_id)
|
|
552
|
+
if not old:
|
|
553
|
+
raise ValueError(f"Key '{key_id}' not found")
|
|
554
|
+
|
|
555
|
+
if old.key_type == KeyType.MASTER:
|
|
556
|
+
return self._rotate_master(old, reason)
|
|
557
|
+
|
|
558
|
+
old_fingerprint = old.fingerprint
|
|
559
|
+
old_version = old.version
|
|
560
|
+
|
|
561
|
+
if old.key_type == KeyType.TEAM:
|
|
562
|
+
new_raw = secrets.token_bytes(32)
|
|
563
|
+
else:
|
|
564
|
+
self._ensure_master()
|
|
565
|
+
info = f"skcapstone:kms:{old.key_type.value}:{old.label}:v{old.version + 1}".encode()
|
|
566
|
+
new_raw = _derive_key(self._master_material, info, length=32)
|
|
567
|
+
|
|
568
|
+
old.status = KeyStatus.ROTATED
|
|
569
|
+
old.rotated_at = datetime.now(timezone.utc)
|
|
570
|
+
self._update_record(old)
|
|
571
|
+
|
|
572
|
+
new_record = KeyRecord(
|
|
573
|
+
key_id=_key_id(old.label, old.key_type, old.version + 1),
|
|
574
|
+
key_type=old.key_type,
|
|
575
|
+
label=old.label,
|
|
576
|
+
parent_key_id=old.parent_key_id,
|
|
577
|
+
fingerprint=_key_fingerprint(new_raw),
|
|
578
|
+
version=old.version + 1,
|
|
579
|
+
members=old.members.copy(),
|
|
580
|
+
algorithm=old.algorithm,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
self._save_key_material(new_record.key_id, new_raw)
|
|
584
|
+
self._append_record(new_record)
|
|
585
|
+
|
|
586
|
+
rotation = RotationEntry(
|
|
587
|
+
key_id=old.key_id,
|
|
588
|
+
old_fingerprint=old_fingerprint,
|
|
589
|
+
new_fingerprint=new_record.fingerprint,
|
|
590
|
+
old_version=old_version,
|
|
591
|
+
new_version=new_record.version,
|
|
592
|
+
reason=reason,
|
|
593
|
+
)
|
|
594
|
+
self._append_rotation(rotation)
|
|
595
|
+
|
|
596
|
+
self._audit(
|
|
597
|
+
"KEY_ROTATE",
|
|
598
|
+
f"Rotated key '{old.label}' v{old_version} -> v{new_record.version}",
|
|
599
|
+
metadata={
|
|
600
|
+
"key_id": old.key_id,
|
|
601
|
+
"new_key_id": new_record.key_id,
|
|
602
|
+
"reason": reason,
|
|
603
|
+
},
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
return new_record
|
|
607
|
+
|
|
608
|
+
def revoke_key(self, key_id: str, reason: str = "") -> KeyRecord:
|
|
609
|
+
"""Revoke a key — mark it unusable.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
key_id: Key to revoke.
|
|
613
|
+
reason: Optional reason.
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
Updated KeyRecord.
|
|
617
|
+
|
|
618
|
+
Raises:
|
|
619
|
+
ValueError: If key not found.
|
|
620
|
+
"""
|
|
621
|
+
record = self._get_record_by_id(key_id)
|
|
622
|
+
if not record:
|
|
623
|
+
raise ValueError(f"Key '{key_id}' not found")
|
|
624
|
+
|
|
625
|
+
record.status = KeyStatus.REVOKED
|
|
626
|
+
self._update_record(record)
|
|
627
|
+
|
|
628
|
+
key_file = self._keys_dir / f"{key_id}.key.enc"
|
|
629
|
+
if key_file.exists():
|
|
630
|
+
key_file.unlink()
|
|
631
|
+
|
|
632
|
+
self._audit(
|
|
633
|
+
"KEY_REVOKE",
|
|
634
|
+
f"Revoked key '{record.label}' ({key_id})",
|
|
635
|
+
metadata={"key_id": key_id, "reason": reason},
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
return record
|
|
639
|
+
|
|
640
|
+
def get_key(
|
|
641
|
+
self,
|
|
642
|
+
label: str,
|
|
643
|
+
key_type: Optional[KeyType] = None,
|
|
644
|
+
) -> Optional[KeyRecord]:
|
|
645
|
+
"""Look up the latest active key by label.
|
|
646
|
+
|
|
647
|
+
Args:
|
|
648
|
+
label: Key label.
|
|
649
|
+
key_type: Optional filter by key type.
|
|
650
|
+
|
|
651
|
+
Returns:
|
|
652
|
+
KeyRecord if found, None otherwise.
|
|
653
|
+
"""
|
|
654
|
+
records = self._load_records()
|
|
655
|
+
matches = [
|
|
656
|
+
r for r in records
|
|
657
|
+
if r.label == label and r.status == KeyStatus.ACTIVE
|
|
658
|
+
and (key_type is None or r.key_type == key_type)
|
|
659
|
+
]
|
|
660
|
+
if not matches:
|
|
661
|
+
return None
|
|
662
|
+
return max(matches, key=lambda r: r.version)
|
|
663
|
+
|
|
664
|
+
def list_keys(
|
|
665
|
+
self,
|
|
666
|
+
key_type: Optional[KeyType] = None,
|
|
667
|
+
include_inactive: bool = False,
|
|
668
|
+
) -> list[KeyRecord]:
|
|
669
|
+
"""List all managed keys.
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
key_type: Optional filter.
|
|
673
|
+
include_inactive: Include rotated/revoked keys.
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
List of KeyRecords.
|
|
677
|
+
"""
|
|
678
|
+
records = self._load_records()
|
|
679
|
+
if key_type:
|
|
680
|
+
records = [r for r in records if r.key_type == key_type]
|
|
681
|
+
if not include_inactive:
|
|
682
|
+
records = [r for r in records if r.status == KeyStatus.ACTIVE]
|
|
683
|
+
return records
|
|
684
|
+
|
|
685
|
+
def get_key_material(self, key_id: str, agent_name: Optional[str] = None) -> bytes:
|
|
686
|
+
"""Retrieve raw key material (access-controlled for team keys).
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
key_id: Key to retrieve.
|
|
690
|
+
agent_name: Requesting agent (checked against team ACL).
|
|
691
|
+
|
|
692
|
+
Returns:
|
|
693
|
+
Raw key bytes.
|
|
694
|
+
|
|
695
|
+
Raises:
|
|
696
|
+
ValueError: If key not found.
|
|
697
|
+
PermissionError: If agent not in team ACL.
|
|
698
|
+
"""
|
|
699
|
+
record = self._get_record_by_id(key_id)
|
|
700
|
+
if not record:
|
|
701
|
+
raise ValueError(f"Key '{key_id}' not found")
|
|
702
|
+
|
|
703
|
+
if record.status != KeyStatus.ACTIVE:
|
|
704
|
+
raise ValueError(f"Key '{key_id}' is {record.status.value}")
|
|
705
|
+
|
|
706
|
+
if record.key_type == KeyType.TEAM and record.members and agent_name:
|
|
707
|
+
if agent_name not in record.members:
|
|
708
|
+
self._audit(
|
|
709
|
+
"KEY_ACCESS_DENIED",
|
|
710
|
+
f"Agent '{agent_name}' denied access to team key '{record.label}'",
|
|
711
|
+
metadata={"key_id": key_id, "agent": agent_name},
|
|
712
|
+
)
|
|
713
|
+
raise PermissionError(
|
|
714
|
+
f"Agent '{agent_name}' not in team '{record.label}' members"
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
material = self._load_key_material(key_id)
|
|
718
|
+
self._audit(
|
|
719
|
+
"KEY_ACCESS",
|
|
720
|
+
f"Key material accessed: '{record.label}' ({key_id})",
|
|
721
|
+
metadata={"key_id": key_id, "agent": agent_name},
|
|
722
|
+
)
|
|
723
|
+
return material
|
|
724
|
+
|
|
725
|
+
def status(self) -> dict[str, Any]:
|
|
726
|
+
"""Return KMS status summary.
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
Dict with key counts, health, and statistics.
|
|
730
|
+
"""
|
|
731
|
+
records = self._load_records()
|
|
732
|
+
active = [r for r in records if r.status == KeyStatus.ACTIVE]
|
|
733
|
+
rotated = [r for r in records if r.status == KeyStatus.ROTATED]
|
|
734
|
+
revoked = [r for r in records if r.status == KeyStatus.REVOKED]
|
|
735
|
+
|
|
736
|
+
by_type: dict[str, int] = {}
|
|
737
|
+
for r in active:
|
|
738
|
+
by_type[r.key_type.value] = by_type.get(r.key_type.value, 0) + 1
|
|
739
|
+
|
|
740
|
+
expiring_soon = [
|
|
741
|
+
r for r in active
|
|
742
|
+
if r.expires_at and r.expires_at < datetime.now(timezone.utc) + timedelta(days=7)
|
|
743
|
+
]
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
"initialized": bool(active),
|
|
747
|
+
"total_keys": len(records),
|
|
748
|
+
"active": len(active),
|
|
749
|
+
"rotated": len(rotated),
|
|
750
|
+
"revoked": len(revoked),
|
|
751
|
+
"by_type": by_type,
|
|
752
|
+
"expiring_soon": [r.label for r in expiring_soon],
|
|
753
|
+
"kms_dir": str(self._kms_dir),
|
|
754
|
+
"backend_available": _HAS_BACKEND,
|
|
755
|
+
"backend_unsealed": (
|
|
756
|
+
self._backend_kms.is_unsealed if self._backend_kms else False
|
|
757
|
+
),
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
# -------------------------------------------------------------------
|
|
761
|
+
# Internal helpers
|
|
762
|
+
# -------------------------------------------------------------------
|
|
763
|
+
|
|
764
|
+
def _init_backend(self) -> None:
|
|
765
|
+
"""Initialize the sksecurity KMS backend if available."""
|
|
766
|
+
if not _HAS_BACKEND or self._backend_kms is not None:
|
|
767
|
+
return
|
|
768
|
+
|
|
769
|
+
try:
|
|
770
|
+
backend_dir = self._kms_dir / "backend"
|
|
771
|
+
backend_dir.mkdir(parents=True, exist_ok=True)
|
|
772
|
+
store = BackendFileKeyStore(store_dir=backend_dir / "keys")
|
|
773
|
+
self._backend_kms = BackendKMS(
|
|
774
|
+
store=store,
|
|
775
|
+
audit_path=backend_dir / "audit.log",
|
|
776
|
+
)
|
|
777
|
+
passphrase = hashlib.sha256(
|
|
778
|
+
self._get_identity_material()
|
|
779
|
+
).hexdigest()
|
|
780
|
+
self._backend_kms.unseal(passphrase)
|
|
781
|
+
logger.debug("sksecurity KMS backend initialized and unsealed")
|
|
782
|
+
except Exception as exc:
|
|
783
|
+
logger.warning("Failed to initialize sksecurity KMS backend: %s", exc)
|
|
784
|
+
self._backend_kms = None
|
|
785
|
+
|
|
786
|
+
def _ensure_master(self) -> KeyRecord:
|
|
787
|
+
"""Ensure the master key is loaded."""
|
|
788
|
+
if self._master_material is None:
|
|
789
|
+
return self.initialize()
|
|
790
|
+
records = self._load_records()
|
|
791
|
+
master = next((r for r in records if r.key_type == KeyType.MASTER and r.status == KeyStatus.ACTIVE), None)
|
|
792
|
+
if master is None:
|
|
793
|
+
return self.initialize()
|
|
794
|
+
return master
|
|
795
|
+
|
|
796
|
+
def _rotate_master(self, old: KeyRecord, reason: str) -> KeyRecord:
|
|
797
|
+
"""Rotate the master key (re-derives from fresh identity material)."""
|
|
798
|
+
identity_material = self._get_identity_material()
|
|
799
|
+
salt = secrets.token_bytes(16)
|
|
800
|
+
info = f"skcapstone:kms:master:v{old.version + 1}".encode()
|
|
801
|
+
|
|
802
|
+
from cryptography.hazmat.primitives.hashes import SHA256
|
|
803
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
804
|
+
|
|
805
|
+
hkdf = HKDF(algorithm=SHA256(), length=32, salt=salt, info=info)
|
|
806
|
+
new_raw = hkdf.derive(identity_material)
|
|
807
|
+
self._master_material = new_raw
|
|
808
|
+
|
|
809
|
+
old.status = KeyStatus.ROTATED
|
|
810
|
+
old.rotated_at = datetime.now(timezone.utc)
|
|
811
|
+
self._update_record(old)
|
|
812
|
+
|
|
813
|
+
new_record = KeyRecord(
|
|
814
|
+
key_id=_key_id("master", KeyType.MASTER, old.version + 1),
|
|
815
|
+
key_type=KeyType.MASTER,
|
|
816
|
+
label="master",
|
|
817
|
+
fingerprint=_key_fingerprint(new_raw),
|
|
818
|
+
version=old.version + 1,
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
self._save_key_material(new_record.key_id, new_raw)
|
|
822
|
+
self._append_record(new_record)
|
|
823
|
+
|
|
824
|
+
rotation = RotationEntry(
|
|
825
|
+
key_id=old.key_id,
|
|
826
|
+
old_fingerprint=old.fingerprint,
|
|
827
|
+
new_fingerprint=new_record.fingerprint,
|
|
828
|
+
old_version=old.version,
|
|
829
|
+
new_version=new_record.version,
|
|
830
|
+
reason=reason,
|
|
831
|
+
)
|
|
832
|
+
self._append_rotation(rotation)
|
|
833
|
+
|
|
834
|
+
self._audit(
|
|
835
|
+
"MASTER_KEY_ROTATE",
|
|
836
|
+
f"Master key rotated v{old.version} -> v{new_record.version}",
|
|
837
|
+
metadata={"reason": reason},
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
return new_record
|
|
841
|
+
|
|
842
|
+
def _get_identity_material(self) -> bytes:
|
|
843
|
+
"""Get identity keying material from the agent's CapAuth profile."""
|
|
844
|
+
identity_file = self._home / "identity" / "identity.json"
|
|
845
|
+
if identity_file.exists():
|
|
846
|
+
try:
|
|
847
|
+
data = json.loads(identity_file.read_text(encoding="utf-8"))
|
|
848
|
+
fingerprint = data.get("fingerprint", "")
|
|
849
|
+
if fingerprint:
|
|
850
|
+
return f"skcapstone:identity:{fingerprint}".encode()
|
|
851
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
852
|
+
logger.warning("Failed to read identity file %s: %s", identity_file, exc)
|
|
853
|
+
|
|
854
|
+
logger.warning("No identity found for KMS — using random master seed")
|
|
855
|
+
return secrets.token_bytes(64)
|
|
856
|
+
|
|
857
|
+
def _load_records(self) -> list[KeyRecord]:
|
|
858
|
+
"""Load all key records from disk."""
|
|
859
|
+
if not self._keystore_file.exists():
|
|
860
|
+
return []
|
|
861
|
+
try:
|
|
862
|
+
data = json.loads(self._keystore_file.read_text(encoding="utf-8"))
|
|
863
|
+
return [KeyRecord.model_validate(r) for r in data]
|
|
864
|
+
except (json.JSONDecodeError, Exception) as exc:
|
|
865
|
+
logger.warning("Failed to load keystore: %s", exc)
|
|
866
|
+
return []
|
|
867
|
+
|
|
868
|
+
def _save_records(self, records: list[KeyRecord]) -> None:
|
|
869
|
+
"""Write all key records to disk."""
|
|
870
|
+
self._kms_dir.mkdir(parents=True, exist_ok=True)
|
|
871
|
+
data = [r.model_dump(mode="json") for r in records]
|
|
872
|
+
self._keystore_file.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
|
|
873
|
+
|
|
874
|
+
def _append_record(self, record: KeyRecord) -> None:
|
|
875
|
+
"""Add a new record to the keystore."""
|
|
876
|
+
records = self._load_records()
|
|
877
|
+
records.append(record)
|
|
878
|
+
self._save_records(records)
|
|
879
|
+
|
|
880
|
+
def _update_record(self, updated: KeyRecord) -> None:
|
|
881
|
+
"""Update an existing record in the keystore."""
|
|
882
|
+
records = self._load_records()
|
|
883
|
+
for i, r in enumerate(records):
|
|
884
|
+
if r.key_id == updated.key_id and r.version == updated.version:
|
|
885
|
+
records[i] = updated
|
|
886
|
+
break
|
|
887
|
+
self._save_records(records)
|
|
888
|
+
|
|
889
|
+
def _get_record_by_id(self, key_id: str) -> Optional[KeyRecord]:
|
|
890
|
+
"""Find a record by key_id."""
|
|
891
|
+
records = self._load_records()
|
|
892
|
+
matches = [r for r in records if r.key_id == key_id]
|
|
893
|
+
return matches[-1] if matches else None
|
|
894
|
+
|
|
895
|
+
def _save_key_material(self, key_id: str, raw: bytes) -> None:
|
|
896
|
+
"""Encrypt and save raw key material to disk using AES-256-GCM."""
|
|
897
|
+
self._keys_dir.mkdir(parents=True, exist_ok=True)
|
|
898
|
+
enc_key = self._get_encryption_key()
|
|
899
|
+
encrypted = _encrypt_at_rest(raw, enc_key)
|
|
900
|
+
(self._keys_dir / f"{key_id}.key.enc").write_bytes(encrypted)
|
|
901
|
+
|
|
902
|
+
def _load_key_material(self, key_id: str) -> bytes:
|
|
903
|
+
"""Load and decrypt key material from disk."""
|
|
904
|
+
key_file = self._keys_dir / f"{key_id}.key.enc"
|
|
905
|
+
if not key_file.exists():
|
|
906
|
+
raise ValueError(f"Key material not found for '{key_id}'")
|
|
907
|
+
enc_key = self._get_encryption_key()
|
|
908
|
+
return _decrypt_at_rest(key_file.read_bytes(), enc_key)
|
|
909
|
+
|
|
910
|
+
def _get_encryption_key(self) -> bytes:
|
|
911
|
+
"""Get the encryption key for at-rest key storage.
|
|
912
|
+
|
|
913
|
+
Uses a deterministic derivation from the agent's identity
|
|
914
|
+
fingerprint so keys can be decrypted without storing a
|
|
915
|
+
separate passphrase.
|
|
916
|
+
"""
|
|
917
|
+
identity_material = self._get_identity_material()
|
|
918
|
+
return _derive_key(identity_material, b"skcapstone:kms:storage-encryption", length=32)
|
|
919
|
+
|
|
920
|
+
def _append_rotation(self, entry: RotationEntry) -> None:
|
|
921
|
+
"""Append a rotation event to the rotation log."""
|
|
922
|
+
log: list[dict] = []
|
|
923
|
+
if self._rotation_log.exists():
|
|
924
|
+
try:
|
|
925
|
+
log = json.loads(self._rotation_log.read_text(encoding="utf-8"))
|
|
926
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
927
|
+
logger.warning("Failed to read rotation log, starting fresh: %s", exc)
|
|
928
|
+
log.append(entry.model_dump(mode="json"))
|
|
929
|
+
self._rotation_log.write_text(json.dumps(log, indent=2, default=str), encoding="utf-8")
|
|
930
|
+
|
|
931
|
+
def _audit(
|
|
932
|
+
self,
|
|
933
|
+
event_type: str,
|
|
934
|
+
detail: str,
|
|
935
|
+
metadata: Optional[dict] = None,
|
|
936
|
+
) -> None:
|
|
937
|
+
"""Log a KMS event to the security audit trail."""
|
|
938
|
+
try:
|
|
939
|
+
from .pillars.security import audit_event
|
|
940
|
+
audit_event(self._home, event_type, detail, agent="kms", metadata=metadata)
|
|
941
|
+
except Exception:
|
|
942
|
+
logger.debug("Audit log unavailable: %s — %s", event_type, detail)
|