@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
|
@@ -8,6 +8,7 @@ No corporate SSO. No OAuth dance. Cryptographic proof of self.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
|
+
import logging
|
|
11
12
|
import subprocess
|
|
12
13
|
from datetime import datetime, timezone
|
|
13
14
|
from pathlib import Path
|
|
@@ -15,6 +16,8 @@ from typing import Optional
|
|
|
15
16
|
|
|
16
17
|
from ..models import IdentityState, PillarStatus
|
|
17
18
|
|
|
19
|
+
logger = logging.getLogger("skcapstone.identity")
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
def generate_identity(
|
|
20
23
|
home: Path,
|
|
@@ -44,20 +47,12 @@ def generate_identity(
|
|
|
44
47
|
status=PillarStatus.DEGRADED,
|
|
45
48
|
)
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
name=name,
|
|
52
|
-
email=state.email,
|
|
53
|
-
output_dir=str(identity_dir),
|
|
54
|
-
)
|
|
55
|
-
state.fingerprint = fingerprint
|
|
56
|
-
state.key_path = identity_dir / "agent.pub"
|
|
50
|
+
capauth_state = _try_init_capauth(name, state.email, identity_dir)
|
|
51
|
+
if capauth_state is not None:
|
|
52
|
+
state.fingerprint = capauth_state.fingerprint
|
|
53
|
+
state.key_path = capauth_state.key_path
|
|
57
54
|
state.status = PillarStatus.ACTIVE
|
|
58
|
-
|
|
59
|
-
# Reason: CapAuth not installed or key generation failed —
|
|
60
|
-
# record identity metadata anyway so agent has a name
|
|
55
|
+
else:
|
|
61
56
|
state.fingerprint = _generate_placeholder_fingerprint(name)
|
|
62
57
|
state.status = PillarStatus.DEGRADED
|
|
63
58
|
|
|
@@ -68,11 +63,86 @@ def generate_identity(
|
|
|
68
63
|
"created_at": state.created_at.isoformat() if state.created_at else None,
|
|
69
64
|
"capauth_managed": state.status == PillarStatus.ACTIVE,
|
|
70
65
|
}
|
|
71
|
-
(identity_dir / "identity.json").write_text(json.dumps(identity_manifest, indent=2))
|
|
66
|
+
(identity_dir / "identity.json").write_text(json.dumps(identity_manifest, indent=2), encoding="utf-8")
|
|
72
67
|
|
|
73
68
|
return state
|
|
74
69
|
|
|
75
70
|
|
|
71
|
+
def _try_init_capauth(
|
|
72
|
+
name: str, email: str, identity_dir: Path
|
|
73
|
+
) -> Optional[IdentityState]:
|
|
74
|
+
"""Try to create or load a real CapAuth identity.
|
|
75
|
+
|
|
76
|
+
Attempts (in order):
|
|
77
|
+
1. Load an existing CapAuth profile from ~/.capauth/
|
|
78
|
+
2. Create a new profile via capauth.profile.init_profile()
|
|
79
|
+
3. Fall back to legacy capauth.keys.generate_keypair()
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
name: Agent display name.
|
|
83
|
+
email: Agent email.
|
|
84
|
+
identity_dir: Path to ~/.skcapstone/identity/.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
IdentityState with real PGP keys, or None if CapAuth unavailable.
|
|
88
|
+
"""
|
|
89
|
+
# Try loading an existing CapAuth profile first
|
|
90
|
+
try:
|
|
91
|
+
from capauth.profile import load_profile # type: ignore[import-untyped]
|
|
92
|
+
|
|
93
|
+
profile = load_profile()
|
|
94
|
+
return IdentityState(
|
|
95
|
+
fingerprint=profile.key_info.fingerprint,
|
|
96
|
+
key_path=Path(profile.key_info.public_key_path),
|
|
97
|
+
name=profile.entity.name,
|
|
98
|
+
email=profile.entity.email,
|
|
99
|
+
status=PillarStatus.ACTIVE,
|
|
100
|
+
)
|
|
101
|
+
except ImportError:
|
|
102
|
+
return None
|
|
103
|
+
except Exception as exc:
|
|
104
|
+
logger.debug("Could not load existing CapAuth profile: %s", exc)
|
|
105
|
+
|
|
106
|
+
# No existing profile — try creating one
|
|
107
|
+
try:
|
|
108
|
+
from capauth.profile import init_profile # type: ignore[import-untyped]
|
|
109
|
+
|
|
110
|
+
profile = init_profile(
|
|
111
|
+
name=name,
|
|
112
|
+
email=email,
|
|
113
|
+
passphrase="",
|
|
114
|
+
)
|
|
115
|
+
return IdentityState(
|
|
116
|
+
fingerprint=profile.key_info.fingerprint,
|
|
117
|
+
key_path=Path(profile.key_info.public_key_path),
|
|
118
|
+
name=profile.entity.name,
|
|
119
|
+
email=profile.entity.email,
|
|
120
|
+
status=PillarStatus.ACTIVE,
|
|
121
|
+
)
|
|
122
|
+
except Exception as exc:
|
|
123
|
+
logger.debug("Could not create CapAuth profile: %s", exc)
|
|
124
|
+
|
|
125
|
+
# Legacy fallback: capauth.keys.generate_keypair
|
|
126
|
+
try:
|
|
127
|
+
from capauth.keys import generate_keypair # type: ignore[import-untyped]
|
|
128
|
+
|
|
129
|
+
_pub_key, fingerprint = generate_keypair(
|
|
130
|
+
name=name,
|
|
131
|
+
email=email,
|
|
132
|
+
output_dir=str(identity_dir),
|
|
133
|
+
)
|
|
134
|
+
return IdentityState(
|
|
135
|
+
fingerprint=fingerprint,
|
|
136
|
+
key_path=identity_dir / "agent.pub",
|
|
137
|
+
name=name,
|
|
138
|
+
email=email,
|
|
139
|
+
status=PillarStatus.ACTIVE,
|
|
140
|
+
)
|
|
141
|
+
except Exception as exc:
|
|
142
|
+
logger.debug("CapAuth keypair generation failed: %s", exc)
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
76
146
|
def _generate_placeholder_fingerprint(name: str) -> str:
|
|
77
147
|
"""Generate a deterministic placeholder fingerprint from the agent name.
|
|
78
148
|
|
|
@@ -31,7 +31,9 @@ def initialize_memory(home: Path, memory_home: Optional[Path] = None) -> MemoryS
|
|
|
31
31
|
Returns:
|
|
32
32
|
MemoryState after initialization.
|
|
33
33
|
"""
|
|
34
|
-
|
|
34
|
+
# Use agent-specific memory directory
|
|
35
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
|
|
36
|
+
memory_dir = home / "agents" / agent_name / "memory"
|
|
35
37
|
memory_dir.mkdir(parents=True, exist_ok=True)
|
|
36
38
|
|
|
37
39
|
for layer in MemoryLayer:
|
|
@@ -2,16 +2,42 @@
|
|
|
2
2
|
Security pillar — SKSecurity integration.
|
|
3
3
|
|
|
4
4
|
Audit everything. Detect threats. Protect the sovereign.
|
|
5
|
+
|
|
6
|
+
Audit log format is JSONL (one JSON object per line), making entries
|
|
7
|
+
both machine-parseable and append-only safe. Each entry includes a
|
|
8
|
+
timestamp, event type, detail, and the hostname that generated it.
|
|
5
9
|
"""
|
|
6
10
|
|
|
7
11
|
from __future__ import annotations
|
|
8
12
|
|
|
9
13
|
import json
|
|
14
|
+
import socket
|
|
10
15
|
from datetime import datetime, timezone
|
|
11
16
|
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
12
20
|
|
|
13
21
|
from ..models import PillarStatus, SecurityState
|
|
14
22
|
|
|
23
|
+
AUDIT_LOG_NAME = "audit.log"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AuditEntry(BaseModel):
|
|
27
|
+
"""A single structured audit log entry.
|
|
28
|
+
|
|
29
|
+
Each entry is serialised as one JSON line in the append-only log.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
timestamp: str = Field(
|
|
33
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
34
|
+
)
|
|
35
|
+
event_type: str
|
|
36
|
+
detail: str
|
|
37
|
+
host: str = Field(default_factory=socket.gethostname)
|
|
38
|
+
agent: Optional[str] = None
|
|
39
|
+
metadata: Optional[dict] = None
|
|
40
|
+
|
|
15
41
|
|
|
16
42
|
def initialize_security(home: Path) -> SecurityState:
|
|
17
43
|
"""Initialize security layer for the agent.
|
|
@@ -32,52 +58,119 @@ def initialize_security(home: Path) -> SecurityState:
|
|
|
32
58
|
try:
|
|
33
59
|
import sksecurity # type: ignore[import-untyped]
|
|
34
60
|
|
|
35
|
-
|
|
61
|
+
sksecurity_version = getattr(sksecurity, "__version__", None)
|
|
36
62
|
except ImportError:
|
|
37
63
|
security_config = {
|
|
38
64
|
"note": "Install sksecurity (pip install sksecurity) for full security",
|
|
39
65
|
"audit_enabled": True,
|
|
40
66
|
}
|
|
41
|
-
(security_dir / "security.json").write_text(json.dumps(security_config, indent=2))
|
|
67
|
+
(security_dir / "security.json").write_text(json.dumps(security_config, indent=2), encoding="utf-8")
|
|
42
68
|
state.status = PillarStatus.DEGRADED
|
|
43
69
|
_init_audit_log(security_dir)
|
|
44
70
|
return state
|
|
45
71
|
|
|
46
72
|
_init_audit_log(security_dir)
|
|
47
73
|
|
|
74
|
+
audit_log = security_dir / AUDIT_LOG_NAME
|
|
75
|
+
if sksecurity_version and audit_log.exists():
|
|
76
|
+
state.status = PillarStatus.ACTIVE
|
|
77
|
+
else:
|
|
78
|
+
state.status = PillarStatus.DEGRADED
|
|
79
|
+
|
|
48
80
|
baseline = {
|
|
49
81
|
"threats_detected": 0,
|
|
50
82
|
"last_scan": None,
|
|
51
83
|
"audit_enabled": True,
|
|
84
|
+
"sksecurity_version": sksecurity_version,
|
|
52
85
|
"initialized_at": datetime.now(timezone.utc).isoformat(),
|
|
53
86
|
}
|
|
54
|
-
(security_dir / "security.json").write_text(json.dumps(baseline, indent=2))
|
|
87
|
+
(security_dir / "security.json").write_text(json.dumps(baseline, indent=2), encoding="utf-8")
|
|
55
88
|
|
|
56
89
|
return state
|
|
57
90
|
|
|
58
91
|
|
|
59
92
|
def _init_audit_log(security_dir: Path) -> None:
|
|
60
|
-
"""Create the audit log file with a
|
|
61
|
-
audit_log = security_dir /
|
|
93
|
+
"""Create the audit log file with a structured INIT entry."""
|
|
94
|
+
audit_log = security_dir / AUDIT_LOG_NAME
|
|
62
95
|
if not audit_log.exists():
|
|
63
|
-
|
|
64
|
-
|
|
96
|
+
entry = AuditEntry(
|
|
97
|
+
event_type="INIT",
|
|
98
|
+
detail="SKCapstone security audit log created",
|
|
99
|
+
)
|
|
100
|
+
audit_log.write_text(entry.model_dump_json() + "\n", encoding="utf-8")
|
|
101
|
+
|
|
65
102
|
|
|
103
|
+
def audit_event(
|
|
104
|
+
home: Path,
|
|
105
|
+
event_type: str,
|
|
106
|
+
detail: str,
|
|
107
|
+
agent: Optional[str] = None,
|
|
108
|
+
metadata: Optional[dict] = None,
|
|
109
|
+
) -> AuditEntry:
|
|
110
|
+
"""Append a structured event to the audit log.
|
|
66
111
|
|
|
67
|
-
|
|
68
|
-
|
|
112
|
+
Each event is written as a single JSON line (JSONL format),
|
|
113
|
+
keeping the log append-only and machine-parseable.
|
|
69
114
|
|
|
70
115
|
Args:
|
|
71
116
|
home: Agent home directory.
|
|
72
|
-
event_type: Event category (INIT, AUTH, MEMORY, TRUST,
|
|
117
|
+
event_type: Event category (INIT, AUTH, MEMORY, TRUST, SYNC,
|
|
118
|
+
TOKEN_ISSUE, TOKEN_REVOKE, SECURITY, etc.).
|
|
73
119
|
detail: Human-readable event description.
|
|
120
|
+
agent: Optional agent name that triggered the event.
|
|
121
|
+
metadata: Optional dict of extra structured data.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
AuditEntry: The entry that was written.
|
|
74
125
|
"""
|
|
75
126
|
security_dir = home / "security"
|
|
76
127
|
security_dir.mkdir(parents=True, exist_ok=True)
|
|
77
|
-
audit_log = security_dir /
|
|
128
|
+
audit_log = security_dir / AUDIT_LOG_NAME
|
|
129
|
+
|
|
130
|
+
entry = AuditEntry(
|
|
131
|
+
event_type=event_type,
|
|
132
|
+
detail=detail,
|
|
133
|
+
agent=agent,
|
|
134
|
+
metadata=metadata,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
with audit_log.open("a", encoding="utf-8") as f:
|
|
138
|
+
f.write(entry.model_dump_json() + "\n")
|
|
139
|
+
|
|
140
|
+
return entry
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def read_audit_log(home: Path, limit: int = 0) -> list[AuditEntry]:
|
|
144
|
+
"""Read and parse the audit log.
|
|
78
145
|
|
|
79
|
-
|
|
80
|
-
|
|
146
|
+
Handles both legacy plain-text entries and new JSONL entries
|
|
147
|
+
gracefully — old lines are wrapped in an AuditEntry with
|
|
148
|
+
event_type="LEGACY".
|
|
81
149
|
|
|
82
|
-
|
|
83
|
-
|
|
150
|
+
Args:
|
|
151
|
+
home: Agent home directory.
|
|
152
|
+
limit: Maximum entries to return (0 = all, newest first).
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
list[AuditEntry]: Parsed audit entries.
|
|
156
|
+
"""
|
|
157
|
+
audit_log = home / "security" / AUDIT_LOG_NAME
|
|
158
|
+
if not audit_log.exists():
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
entries: list[AuditEntry] = []
|
|
162
|
+
for line in audit_log.read_text(encoding="utf-8").splitlines():
|
|
163
|
+
line = line.strip()
|
|
164
|
+
if not line:
|
|
165
|
+
continue
|
|
166
|
+
try:
|
|
167
|
+
data = json.loads(line)
|
|
168
|
+
entries.append(AuditEntry.model_validate(data))
|
|
169
|
+
except (json.JSONDecodeError, Exception):
|
|
170
|
+
# Reason: gracefully handle legacy plain-text log lines
|
|
171
|
+
entries.append(AuditEntry(event_type="LEGACY", detail=line))
|
|
172
|
+
|
|
173
|
+
if limit > 0:
|
|
174
|
+
entries = entries[-limit:]
|
|
175
|
+
|
|
176
|
+
return entries
|
|
@@ -21,6 +21,7 @@ from datetime import datetime, timezone
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
from typing import Optional
|
|
23
23
|
|
|
24
|
+
from .. import SHARED_ROOT
|
|
24
25
|
from ..models import PillarStatus, SyncConfig, SyncState, SyncTransport
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger("skcapstone.sync")
|
|
@@ -56,7 +57,7 @@ def initialize_sync(home: Path, config: Optional[SyncConfig] = None) -> SyncStat
|
|
|
56
57
|
"auto_push": config.auto_push,
|
|
57
58
|
"auto_pull": config.auto_pull,
|
|
58
59
|
}
|
|
59
|
-
(sync_dir / "sync-manifest.json").write_text(json.dumps(manifest, indent=2))
|
|
60
|
+
(sync_dir / "sync-manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
60
61
|
|
|
61
62
|
state = SyncState(
|
|
62
63
|
transport=config.transport,
|
|
@@ -88,9 +89,7 @@ def collect_seed(home: Path, agent_name: str) -> Path:
|
|
|
88
89
|
Returns:
|
|
89
90
|
Path to the generated seed file in the outbox.
|
|
90
91
|
"""
|
|
91
|
-
sync_dir = (home
|
|
92
|
-
if not sync_dir.exists():
|
|
93
|
-
sync_dir = Path("~/.skcapstone/sync").expanduser()
|
|
92
|
+
sync_dir = _resolve_sync_dir(home)
|
|
94
93
|
outbox = sync_dir / "outbox"
|
|
95
94
|
outbox.mkdir(parents=True, exist_ok=True)
|
|
96
95
|
|
|
@@ -107,11 +106,11 @@ def collect_seed(home: Path, agent_name: str) -> Path:
|
|
|
107
106
|
|
|
108
107
|
identity_file = home / "identity" / "identity.json"
|
|
109
108
|
if identity_file.exists():
|
|
110
|
-
seed["identity"] = json.loads(identity_file.read_text())
|
|
109
|
+
seed["identity"] = json.loads(identity_file.read_text(encoding="utf-8"))
|
|
111
110
|
|
|
112
111
|
trust_file = home / "trust" / "trust.json"
|
|
113
112
|
if trust_file.exists():
|
|
114
|
-
seed["trust"] = json.loads(trust_file.read_text())
|
|
113
|
+
seed["trust"] = json.loads(trust_file.read_text(encoding="utf-8"))
|
|
115
114
|
try:
|
|
116
115
|
from .trust import export_febs_for_seed
|
|
117
116
|
|
|
@@ -134,11 +133,11 @@ def collect_seed(home: Path, agent_name: str) -> Path:
|
|
|
134
133
|
|
|
135
134
|
manifest_file = home / "manifest.json"
|
|
136
135
|
if manifest_file.exists():
|
|
137
|
-
seed["manifest"] = json.loads(manifest_file.read_text())
|
|
136
|
+
seed["manifest"] = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
138
137
|
|
|
139
138
|
seed_name = f"{agent_name}-{hostname}-{timestamp.strftime('%Y%m%dT%H%M%SZ')}{SEED_EXTENSION}"
|
|
140
139
|
seed_path = outbox / seed_name
|
|
141
|
-
seed_path.write_text(json.dumps(seed, indent=2, default=str))
|
|
140
|
+
seed_path.write_text(json.dumps(seed, indent=2, default=str), encoding="utf-8")
|
|
142
141
|
|
|
143
142
|
logger.info("Seed collected: %s", seed_path.name)
|
|
144
143
|
return seed_path
|
|
@@ -148,16 +147,20 @@ def gpg_encrypt(
|
|
|
148
147
|
seed_path: Path,
|
|
149
148
|
recipient: Optional[str] = None,
|
|
150
149
|
home: Optional[Path] = None,
|
|
150
|
+
extra_recipients: Optional[list[str]] = None,
|
|
151
151
|
) -> Optional[Path]:
|
|
152
152
|
"""Encrypt a seed file with GPG.
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
Encrypts to the agent's own key AND all known peer fingerprints so
|
|
155
|
+
that every peer in the mesh can independently decrypt the seed they
|
|
156
|
+
receive via Syncthing. Without peer fingerprints, only the sender
|
|
157
|
+
can decrypt — which defeats the purpose of sync.
|
|
156
158
|
|
|
157
159
|
Args:
|
|
158
160
|
seed_path: Path to the plaintext seed file.
|
|
159
|
-
recipient: GPG recipient (fingerprint/email). Auto-detects if None.
|
|
161
|
+
recipient: Primary GPG recipient (fingerprint/email). Auto-detects if None.
|
|
160
162
|
home: Agent home directory for key detection.
|
|
163
|
+
extra_recipients: Additional peer fingerprints to encrypt to.
|
|
161
164
|
|
|
162
165
|
Returns:
|
|
163
166
|
Path to the encrypted file, or None if encryption failed.
|
|
@@ -166,21 +169,32 @@ def gpg_encrypt(
|
|
|
166
169
|
logger.error("gpg not found in PATH — cannot encrypt")
|
|
167
170
|
return None
|
|
168
171
|
|
|
172
|
+
agent_home = home or Path(SHARED_ROOT).expanduser()
|
|
173
|
+
|
|
169
174
|
if recipient is None:
|
|
170
|
-
agent_home = home or Path("~/.skcapstone").expanduser()
|
|
171
175
|
recipient = _detect_gpg_key(agent_home)
|
|
172
176
|
|
|
173
177
|
if recipient is None:
|
|
174
178
|
logger.error("No GPG key found for encryption")
|
|
175
179
|
return None
|
|
176
180
|
|
|
181
|
+
# Build recipient list: own key + all known peers
|
|
182
|
+
all_recipients = [recipient]
|
|
183
|
+
if extra_recipients:
|
|
184
|
+
all_recipients.extend(r for r in extra_recipients if r and r != recipient)
|
|
185
|
+
|
|
177
186
|
encrypted_path = seed_path.parent / (seed_path.name + ".gpg")
|
|
178
187
|
|
|
188
|
+
recipient_args: list[str] = []
|
|
189
|
+
for r in all_recipients:
|
|
190
|
+
recipient_args += ["--recipient", r]
|
|
191
|
+
|
|
179
192
|
try:
|
|
180
193
|
subprocess.run(
|
|
181
194
|
[
|
|
182
195
|
"gpg", "--batch", "--yes", "--trust-model", "always",
|
|
183
|
-
"--armor", "--encrypt",
|
|
196
|
+
"--armor", "--encrypt",
|
|
197
|
+
*recipient_args,
|
|
184
198
|
"--output", str(encrypted_path), str(seed_path),
|
|
185
199
|
],
|
|
186
200
|
capture_output=True,
|
|
@@ -188,7 +202,10 @@ def gpg_encrypt(
|
|
|
188
202
|
check=True,
|
|
189
203
|
timeout=30,
|
|
190
204
|
)
|
|
191
|
-
logger.info(
|
|
205
|
+
logger.info(
|
|
206
|
+
"Encrypted: %s -> %s (recipients: %d)",
|
|
207
|
+
seed_path.name, encrypted_path.name, len(all_recipients),
|
|
208
|
+
)
|
|
192
209
|
return encrypted_path
|
|
193
210
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
|
|
194
211
|
logger.error("GPG encryption failed: %s", exc)
|
|
@@ -235,6 +252,9 @@ def push_seed(home: Path, agent_name: str, encrypt: bool = True) -> Optional[Pat
|
|
|
235
252
|
This is the high-level 'push' operation. After this, Syncthing
|
|
236
253
|
(or git) handles propagation to all peers automatically.
|
|
237
254
|
|
|
255
|
+
Reads peer_fingerprints from the sync config so seeds are encrypted
|
|
256
|
+
to all known peers, not just the sender's own key.
|
|
257
|
+
|
|
238
258
|
Args:
|
|
239
259
|
home: Agent home directory.
|
|
240
260
|
agent_name: Agent display name.
|
|
@@ -246,7 +266,8 @@ def push_seed(home: Path, agent_name: str, encrypt: bool = True) -> Optional[Pat
|
|
|
246
266
|
seed_path = collect_seed(home, agent_name)
|
|
247
267
|
|
|
248
268
|
if encrypt:
|
|
249
|
-
|
|
269
|
+
peer_fingerprints = _load_peer_fingerprints(home)
|
|
270
|
+
encrypted = gpg_encrypt(seed_path, home=home, extra_recipients=peer_fingerprints)
|
|
250
271
|
if encrypted:
|
|
251
272
|
seed_path.unlink()
|
|
252
273
|
return encrypted
|
|
@@ -255,6 +276,29 @@ def push_seed(home: Path, agent_name: str, encrypt: bool = True) -> Optional[Pat
|
|
|
255
276
|
return seed_path
|
|
256
277
|
|
|
257
278
|
|
|
279
|
+
def _load_peer_fingerprints(home: Path) -> list[str]:
|
|
280
|
+
"""Load known peer GPG fingerprints from sync config.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
home: Agent home directory.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
List of peer fingerprint strings (may be empty).
|
|
287
|
+
"""
|
|
288
|
+
config_file = home / "config" / "config.yaml"
|
|
289
|
+
if not config_file.exists():
|
|
290
|
+
return []
|
|
291
|
+
try:
|
|
292
|
+
import yaml as _yaml
|
|
293
|
+
data = _yaml.safe_load(config_file.read_text(encoding="utf-8")) or {}
|
|
294
|
+
sync_data = data.get("sync", {})
|
|
295
|
+
peers = sync_data.get("peer_fingerprints", [])
|
|
296
|
+
return [str(p) for p in peers if p]
|
|
297
|
+
except Exception as exc:
|
|
298
|
+
logger.debug("Could not load peer fingerprints: %s", exc)
|
|
299
|
+
return []
|
|
300
|
+
|
|
301
|
+
|
|
258
302
|
def pull_seeds(home: Path, decrypt: bool = True) -> list[dict]:
|
|
259
303
|
"""Pull and process seed files from the inbox.
|
|
260
304
|
|
|
@@ -293,7 +337,7 @@ def pull_seeds(home: Path, decrypt: bool = True) -> list[dict]:
|
|
|
293
337
|
|
|
294
338
|
if seed_path.suffix == ".json" or seed_path.name.endswith(SEED_EXTENSION):
|
|
295
339
|
try:
|
|
296
|
-
data = json.loads(seed_path.read_text())
|
|
340
|
+
data = json.loads(seed_path.read_text(encoding="utf-8"))
|
|
297
341
|
seeds.append(data)
|
|
298
342
|
|
|
299
343
|
if "memory_entries" in data:
|
|
@@ -304,7 +348,7 @@ def pull_seeds(home: Path, decrypt: bool = True) -> list[dict]:
|
|
|
304
348
|
if imported:
|
|
305
349
|
logger.info("Imported %d memories from seed %s", imported, seed_path.name)
|
|
306
350
|
except Exception as exc:
|
|
307
|
-
logger.
|
|
351
|
+
logger.warning("Could not import memories from seed %s: %s", seed_path.name, exc)
|
|
308
352
|
|
|
309
353
|
if "febs" in data:
|
|
310
354
|
try:
|
|
@@ -314,7 +358,7 @@ def pull_seeds(home: Path, decrypt: bool = True) -> list[dict]:
|
|
|
314
358
|
if feb_imported:
|
|
315
359
|
logger.info("Imported %d FEB(s) from seed %s", feb_imported, seed_path.name)
|
|
316
360
|
except Exception as exc:
|
|
317
|
-
logger.
|
|
361
|
+
logger.warning("Could not import FEBs from seed %s: %s", seed_path.name, exc)
|
|
318
362
|
|
|
319
363
|
archive.mkdir(exist_ok=True)
|
|
320
364
|
seed_path.rename(archive / seed_path.name)
|
|
@@ -343,7 +387,7 @@ def discover_sync(home: Path) -> SyncState:
|
|
|
343
387
|
return SyncState(sync_path=sync_dir, status=PillarStatus.DEGRADED)
|
|
344
388
|
|
|
345
389
|
try:
|
|
346
|
-
data = json.loads(manifest_file.read_text())
|
|
390
|
+
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
347
391
|
except (json.JSONDecodeError, OSError):
|
|
348
392
|
return SyncState(sync_path=sync_dir, status=PillarStatus.DEGRADED)
|
|
349
393
|
|
|
@@ -370,11 +414,19 @@ def discover_sync(home: Path) -> SyncState:
|
|
|
370
414
|
|
|
371
415
|
|
|
372
416
|
def _resolve_sync_dir(home: Path) -> Path:
|
|
373
|
-
"""Resolve the sync directory path.
|
|
417
|
+
"""Resolve the sync directory path.
|
|
418
|
+
|
|
419
|
+
Prefers the explicitly-passed home/sync (used by tests and
|
|
420
|
+
single-agent mode), then falls back to SHARED_ROOT/sync for
|
|
421
|
+
multi-agent mode.
|
|
422
|
+
"""
|
|
374
423
|
sync_dir = home / "sync"
|
|
375
424
|
if sync_dir.exists():
|
|
376
425
|
return sync_dir
|
|
377
|
-
|
|
426
|
+
shared_sync = Path(SHARED_ROOT).expanduser() / "sync"
|
|
427
|
+
if shared_sync.exists():
|
|
428
|
+
return shared_sync
|
|
429
|
+
return sync_dir
|
|
378
430
|
|
|
379
431
|
|
|
380
432
|
def _detect_gpg_key(home: Path) -> Optional[str]:
|
|
@@ -382,7 +434,7 @@ def _detect_gpg_key(home: Path) -> Optional[str]:
|
|
|
382
434
|
identity_file = home / "identity" / "identity.json"
|
|
383
435
|
if identity_file.exists():
|
|
384
436
|
try:
|
|
385
|
-
data = json.loads(identity_file.read_text())
|
|
437
|
+
data = json.loads(identity_file.read_text(encoding="utf-8"))
|
|
386
438
|
fp = data.get("fingerprint")
|
|
387
439
|
if fp and data.get("capauth_managed"):
|
|
388
440
|
return fp
|
|
@@ -460,14 +512,14 @@ def _load_sync_timestamps(sync_dir: Path, state: SyncState) -> None:
|
|
|
460
512
|
state_file = sync_dir / "sync-state.json"
|
|
461
513
|
if state_file.exists():
|
|
462
514
|
try:
|
|
463
|
-
data = json.loads(state_file.read_text())
|
|
515
|
+
data = json.loads(state_file.read_text(encoding="utf-8"))
|
|
464
516
|
if data.get("last_push"):
|
|
465
517
|
state.last_push = datetime.fromisoformat(data["last_push"])
|
|
466
518
|
if data.get("last_pull"):
|
|
467
519
|
state.last_pull = datetime.fromisoformat(data["last_pull"])
|
|
468
520
|
state.peers_known = data.get("peers_known", 0)
|
|
469
|
-
except (json.JSONDecodeError, OSError, ValueError):
|
|
470
|
-
|
|
521
|
+
except (json.JSONDecodeError, OSError, ValueError) as exc:
|
|
522
|
+
logger.debug("Could not load sync timestamps: %s", exc)
|
|
471
523
|
|
|
472
524
|
|
|
473
525
|
def save_sync_state(sync_dir: Path, state: SyncState) -> None:
|
|
@@ -483,4 +535,4 @@ def save_sync_state(sync_dir: Path, state: SyncState) -> None:
|
|
|
483
535
|
"peers_known": state.peers_known,
|
|
484
536
|
"seed_count": state.seed_count,
|
|
485
537
|
}
|
|
486
|
-
(sync_dir / "sync-state.json").write_text(json.dumps(data, indent=2))
|
|
538
|
+
(sync_dir / "sync-state.json").write_text(json.dumps(data, indent=2), encoding="utf-8")
|