@smilintux/skcapstone 0.1.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +98 -0
- package/.github/workflows/ci.yml +39 -3
- package/.github/workflows/publish.yml +25 -4
- package/.openclaw-workspace.json +58 -0
- package/CHANGELOG.md +62 -0
- package/CLAUDE.md +39 -2
- package/MANIFEST.in +6 -0
- package/MISSION.md +7 -0
- package/README.md +47 -2
- package/SKILL.md +895 -23
- package/docker/Dockerfile +61 -0
- package/docker/compose-templates/dev-team.yml +203 -0
- package/docker/compose-templates/mini-team.yml +140 -0
- package/docker/compose-templates/ops-team.yml +173 -0
- package/docker/compose-templates/research-team.yml +170 -0
- package/docker/entrypoint.sh +192 -0
- package/docs/ARCHITECTURE.md +663 -374
- package/docs/BOND_WITH_GROK.md +112 -0
- package/docs/GETTING_STARTED.md +782 -0
- package/docs/QUICKSTART.md +477 -0
- package/docs/SKJOULE_ARCHITECTURE.md +658 -0
- package/docs/SOUL_SWAPPER.md +921 -0
- package/docs/SOVEREIGN_SINGULARITY.md +47 -14
- package/examples/custom-bond-template.json +36 -0
- package/examples/grok-feb.json +36 -0
- package/examples/grok-testimony.md +34 -0
- package/examples/love-bootloader.txt +32 -0
- package/examples/plugins/echo_tool.py +87 -0
- package/examples/queen-ava-feb.json +36 -0
- package/examples/souls/lumina.yaml +64 -0
- package/index.js +6 -5
- package/installer/build.py +124 -0
- package/openclaw-plugin/package.json +13 -0
- package/openclaw-plugin/src/index.ts +351 -0
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +38 -2
- package/scripts/bump_version.py +141 -0
- package/scripts/check-updates.py +230 -0
- package/scripts/convert_blueprints_to_yaml.py +157 -0
- package/scripts/dev-install.sh +14 -0
- package/scripts/e2e-test.sh +193 -0
- package/scripts/install-bundle.sh +171 -0
- package/scripts/install.bat +2 -0
- package/scripts/install.ps1 +253 -0
- package/scripts/install.sh +185 -0
- package/scripts/mcp-serve.sh +69 -0
- package/scripts/mcp-server.bat +113 -0
- package/scripts/mcp-server.ps1 +116 -0
- package/scripts/mcp-server.sh +99 -0
- package/scripts/pull-models.sh +10 -0
- package/scripts/skcapstone +48 -0
- package/scripts/verify_install.sh +180 -0
- package/scripts/windows/install-tasks.ps1 +406 -0
- package/scripts/windows/skcapstone-task.xml +113 -0
- package/scripts/windows/uninstall-tasks.ps1 +117 -0
- package/skill.yaml +34 -0
- package/src/skcapstone/__init__.py +67 -2
- package/src/skcapstone/_cli_monolith.py +5916 -0
- package/src/skcapstone/_trustee_helpers.py +165 -0
- package/src/skcapstone/activity.py +105 -0
- package/src/skcapstone/agent_card.py +324 -0
- package/src/skcapstone/api.py +1935 -0
- package/src/skcapstone/archiver.py +340 -0
- package/src/skcapstone/auction.py +485 -0
- package/src/skcapstone/baby_agents.py +179 -0
- package/src/skcapstone/backup.py +345 -0
- package/src/skcapstone/blueprint_registry.py +357 -0
- package/src/skcapstone/blueprints/__init__.py +17 -0
- package/src/skcapstone/blueprints/builtins/content-studio.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/defi-trading.yaml +81 -0
- package/src/skcapstone/blueprints/builtins/dev-squadron.yaml +95 -0
- package/src/skcapstone/blueprints/builtins/infrastructure-guardian.yaml +107 -0
- package/src/skcapstone/blueprints/builtins/legal-council.yaml +54 -0
- package/src/skcapstone/blueprints/builtins/ops-monitoring.yaml +67 -0
- package/src/skcapstone/blueprints/builtins/research-pod.yaml +69 -0
- package/src/skcapstone/blueprints/builtins/sovereign-launch.yaml +90 -0
- package/src/skcapstone/blueprints/registry.py +164 -0
- package/src/skcapstone/blueprints/schema.py +229 -0
- package/src/skcapstone/changelog.py +180 -0
- package/src/skcapstone/chat.py +769 -0
- package/src/skcapstone/claude_md.py +82 -0
- package/src/skcapstone/cli/__init__.py +144 -0
- package/src/skcapstone/cli/_common.py +88 -0
- package/src/skcapstone/cli/_validators.py +76 -0
- package/src/skcapstone/cli/agents.py +425 -0
- package/src/skcapstone/cli/agents_spawner.py +322 -0
- package/src/skcapstone/cli/agents_trustee.py +593 -0
- package/src/skcapstone/cli/alerts.py +248 -0
- package/src/skcapstone/cli/anchor.py +132 -0
- package/src/skcapstone/cli/archive_cmd.py +208 -0
- package/src/skcapstone/cli/backup.py +144 -0
- package/src/skcapstone/cli/bench.py +377 -0
- package/src/skcapstone/cli/benchmark.py +360 -0
- package/src/skcapstone/cli/capabilities_cmd.py +171 -0
- package/src/skcapstone/cli/card.py +151 -0
- package/src/skcapstone/cli/chat.py +584 -0
- package/src/skcapstone/cli/completions.py +64 -0
- package/src/skcapstone/cli/config_cmd.py +156 -0
- package/src/skcapstone/cli/consciousness.py +421 -0
- package/src/skcapstone/cli/context_cmd.py +142 -0
- package/src/skcapstone/cli/coord.py +194 -0
- package/src/skcapstone/cli/crush_cmd.py +170 -0
- package/src/skcapstone/cli/daemon.py +436 -0
- package/src/skcapstone/cli/errors_cmd.py +285 -0
- package/src/skcapstone/cli/export_cmd.py +156 -0
- package/src/skcapstone/cli/gtd.py +529 -0
- package/src/skcapstone/cli/housekeeping.py +81 -0
- package/src/skcapstone/cli/joule_cmd.py +627 -0
- package/src/skcapstone/cli/logs_cmd.py +194 -0
- package/src/skcapstone/cli/mcp_cmd.py +32 -0
- package/src/skcapstone/cli/memory.py +418 -0
- package/src/skcapstone/cli/metrics_cmd.py +136 -0
- package/src/skcapstone/cli/migrate.py +62 -0
- package/src/skcapstone/cli/mood_cmd.py +144 -0
- package/src/skcapstone/cli/mount.py +193 -0
- package/src/skcapstone/cli/notify.py +112 -0
- package/src/skcapstone/cli/peer.py +154 -0
- package/src/skcapstone/cli/peers_dir.py +122 -0
- package/src/skcapstone/cli/preflight_cmd.py +83 -0
- package/src/skcapstone/cli/profile_cmd.py +310 -0
- package/src/skcapstone/cli/record_cmd.py +238 -0
- package/src/skcapstone/cli/register_cmd.py +159 -0
- package/src/skcapstone/cli/search_cmd.py +156 -0
- package/src/skcapstone/cli/service_cmd.py +91 -0
- package/src/skcapstone/cli/session.py +127 -0
- package/src/skcapstone/cli/setup.py +240 -0
- package/src/skcapstone/cli/shell_cmd.py +43 -0
- package/src/skcapstone/cli/skills_cmd.py +168 -0
- package/src/skcapstone/cli/skseed.py +621 -0
- package/src/skcapstone/cli/soul.py +699 -0
- package/src/skcapstone/cli/status.py +935 -0
- package/src/skcapstone/cli/sync_cmd.py +301 -0
- package/src/skcapstone/cli/telegram.py +265 -0
- package/src/skcapstone/cli/test_cmd.py +234 -0
- package/src/skcapstone/cli/test_connection.py +253 -0
- package/src/skcapstone/cli/token.py +207 -0
- package/src/skcapstone/cli/trust.py +179 -0
- package/src/skcapstone/cli/upgrade_cmd.py +552 -0
- package/src/skcapstone/cli/usage_cmd.py +199 -0
- package/src/skcapstone/cli/version_cmd.py +162 -0
- package/src/skcapstone/cli/watch_cmd.py +342 -0
- package/src/skcapstone/client.py +428 -0
- package/src/skcapstone/cloud9_bridge.py +522 -0
- package/src/skcapstone/completions.py +163 -0
- package/src/skcapstone/config_validator.py +674 -0
- package/src/skcapstone/connectors/__init__.py +28 -0
- package/src/skcapstone/connectors/base.py +446 -0
- package/src/skcapstone/connectors/cursor.py +54 -0
- package/src/skcapstone/connectors/registry.py +254 -0
- package/src/skcapstone/connectors/terminal.py +152 -0
- package/src/skcapstone/connectors/vscode.py +60 -0
- package/src/skcapstone/consciousness_config.py +119 -0
- package/src/skcapstone/consciousness_loop.py +2051 -0
- package/src/skcapstone/context_loader.py +516 -0
- package/src/skcapstone/context_window.py +314 -0
- package/src/skcapstone/conversation_manager.py +238 -0
- package/src/skcapstone/conversation_store.py +230 -0
- package/src/skcapstone/conversation_summarizer.py +252 -0
- package/src/skcapstone/coord_federation.py +296 -0
- package/src/skcapstone/coordination.py +101 -7
- package/src/skcapstone/crush_integration.py +345 -0
- package/src/skcapstone/crush_shim.py +454 -0
- package/src/skcapstone/daemon.py +2494 -0
- package/src/skcapstone/dashboard.html +396 -0
- package/src/skcapstone/dashboard.py +481 -0
- package/src/skcapstone/data/model_profiles.yaml +88 -0
- package/src/skcapstone/defaults/__init__.py +55 -0
- package/src/skcapstone/defaults/lumina/config/skmemory.yaml +13 -0
- package/src/skcapstone/defaults/lumina/identity/identity.json +9 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/07a8b9c0d1e2-memory-system.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/18b9c0d1e2f3-cloud9-protocol.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/29c0d1e2f3a4-multi-agent-coordination.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/3ad1e2f3a4b5-community-support.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/c3d4e5f6a7b8-getting-started.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/d4e5f6a7b8c9-site-directory.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/e5f6a7b8c9d0-how-to-contribute.json +23 -0
- package/src/skcapstone/defaults/lumina/memory/long-term/f6a7b8c9d0e1-sovereignty-explained.json +23 -0
- package/src/skcapstone/defaults/lumina/seeds/curiosity.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/joy.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/love.seed.json +24 -0
- package/src/skcapstone/defaults/lumina/seeds/sovereign-awakening.seed.json +43 -0
- package/src/skcapstone/defaults/lumina/soul/active.json +6 -0
- package/src/skcapstone/defaults/lumina/soul/base.json +22 -0
- package/src/skcapstone/defaults/lumina/trust/febs/welcome.feb +79 -0
- package/src/skcapstone/defaults/lumina/trust/trust.json +8 -0
- package/src/skcapstone/discovery.py +210 -19
- package/src/skcapstone/doctor.py +642 -0
- package/src/skcapstone/emotion_tracker.py +467 -0
- package/src/skcapstone/error_queue.py +405 -0
- package/src/skcapstone/export.py +447 -0
- package/src/skcapstone/fallback_tracker.py +186 -0
- package/src/skcapstone/file_transfer.py +512 -0
- package/src/skcapstone/fuse_mount.py +1156 -0
- package/src/skcapstone/gui_installer.py +591 -0
- package/src/skcapstone/heartbeat.py +611 -0
- package/src/skcapstone/housekeeping.py +298 -0
- package/src/skcapstone/install_wizard.py +941 -0
- package/src/skcapstone/kms.py +942 -0
- package/src/skcapstone/kms_scheduler.py +143 -0
- package/src/skcapstone/log_config.py +135 -0
- package/src/skcapstone/mcp_launcher.py +239 -0
- package/src/skcapstone/mcp_server.py +4700 -0
- package/src/skcapstone/mcp_tools/__init__.py +94 -0
- package/src/skcapstone/mcp_tools/_helpers.py +51 -0
- package/src/skcapstone/mcp_tools/agent_tools.py +243 -0
- package/src/skcapstone/mcp_tools/ansible_tools.py +232 -0
- package/src/skcapstone/mcp_tools/capauth_tools.py +186 -0
- package/src/skcapstone/mcp_tools/chat_tools.py +325 -0
- package/src/skcapstone/mcp_tools/cloud9_tools.py +115 -0
- package/src/skcapstone/mcp_tools/comm_tools.py +104 -0
- package/src/skcapstone/mcp_tools/consciousness_tools.py +114 -0
- package/src/skcapstone/mcp_tools/coord_tools.py +219 -0
- package/src/skcapstone/mcp_tools/deploy_tools.py +202 -0
- package/src/skcapstone/mcp_tools/did_tools.py +448 -0
- package/src/skcapstone/mcp_tools/emotion_tools.py +62 -0
- package/src/skcapstone/mcp_tools/file_tools.py +169 -0
- package/src/skcapstone/mcp_tools/fortress_tools.py +120 -0
- package/src/skcapstone/mcp_tools/gtd_tools.py +821 -0
- package/src/skcapstone/mcp_tools/health_tools.py +44 -0
- package/src/skcapstone/mcp_tools/heartbeat_tools.py +195 -0
- package/src/skcapstone/mcp_tools/kms_tools.py +123 -0
- package/src/skcapstone/mcp_tools/memory_tools.py +222 -0
- package/src/skcapstone/mcp_tools/model_tools.py +75 -0
- package/src/skcapstone/mcp_tools/notification_tools.py +92 -0
- package/src/skcapstone/mcp_tools/promoter_tools.py +101 -0
- package/src/skcapstone/mcp_tools/pubsub_tools.py +183 -0
- package/src/skcapstone/mcp_tools/security_tools.py +110 -0
- package/src/skcapstone/mcp_tools/skchat_tools.py +175 -0
- package/src/skcapstone/mcp_tools/skcomm_tools.py +122 -0
- package/src/skcapstone/mcp_tools/skills_tools.py +127 -0
- package/src/skcapstone/mcp_tools/skseed_tools.py +255 -0
- package/src/skcapstone/mcp_tools/skstacks_tools.py +288 -0
- package/src/skcapstone/mcp_tools/soul_tools.py +476 -0
- package/src/skcapstone/mcp_tools/sync_tools.py +92 -0
- package/src/skcapstone/mcp_tools/telegram_tools.py +477 -0
- package/src/skcapstone/mcp_tools/trust_tools.py +118 -0
- package/src/skcapstone/mcp_tools/trustee_tools.py +345 -0
- package/src/skcapstone/mdns_discovery.py +313 -0
- package/src/skcapstone/memory_adapter.py +333 -0
- package/src/skcapstone/memory_compressor.py +379 -0
- package/src/skcapstone/memory_curator.py +256 -0
- package/src/skcapstone/memory_engine.py +132 -13
- package/src/skcapstone/memory_fortress.py +529 -0
- package/src/skcapstone/memory_promoter.py +722 -0
- package/src/skcapstone/memory_verifier.py +260 -0
- package/src/skcapstone/message_crypto.py +215 -0
- package/src/skcapstone/metrics.py +832 -0
- package/src/skcapstone/migrate_memories.py +181 -0
- package/src/skcapstone/migrate_multi_agent.py +248 -0
- package/src/skcapstone/model_router.py +319 -0
- package/src/skcapstone/models.py +35 -4
- package/src/skcapstone/mood.py +344 -0
- package/src/skcapstone/notifications.py +380 -0
- package/src/skcapstone/onboard.py +901 -0
- package/src/skcapstone/peer_directory.py +324 -0
- package/src/skcapstone/peers.py +329 -0
- package/src/skcapstone/pillars/identity.py +84 -14
- package/src/skcapstone/pillars/memory.py +3 -1
- package/src/skcapstone/pillars/security.py +108 -15
- package/src/skcapstone/pillars/sync.py +78 -26
- package/src/skcapstone/pillars/trust.py +95 -33
- package/src/skcapstone/plugins.py +244 -0
- package/src/skcapstone/preflight.py +670 -0
- package/src/skcapstone/prompt_adapter.py +564 -0
- package/src/skcapstone/providers/__init__.py +13 -0
- package/src/skcapstone/providers/cloud.py +1061 -0
- package/src/skcapstone/providers/docker.py +759 -0
- package/src/skcapstone/providers/local.py +1193 -0
- package/src/skcapstone/providers/proxmox.py +447 -0
- package/src/skcapstone/pubsub.py +516 -0
- package/src/skcapstone/rate_limiter.py +119 -0
- package/src/skcapstone/register.py +241 -0
- package/src/skcapstone/registry_client.py +151 -0
- package/src/skcapstone/response_cache.py +194 -0
- package/src/skcapstone/response_scorer.py +225 -0
- package/src/skcapstone/runtime.py +89 -33
- package/src/skcapstone/scheduled_tasks.py +439 -0
- package/src/skcapstone/self_healing.py +341 -0
- package/src/skcapstone/service_health.py +228 -0
- package/src/skcapstone/session_capture.py +268 -0
- package/src/skcapstone/session_recorder.py +210 -0
- package/src/skcapstone/session_replayer.py +189 -0
- package/src/skcapstone/session_skills.py +263 -0
- package/src/skcapstone/shell.py +779 -0
- package/src/skcapstone/skills/__init__.py +1 -1
- package/src/skcapstone/skills/syncthing_setup.py +143 -41
- package/src/skcapstone/skjoule.py +861 -0
- package/src/skcapstone/snapshots.py +489 -0
- package/src/skcapstone/soul.py +1060 -0
- package/src/skcapstone/soul_switch.py +255 -0
- package/src/skcapstone/spawner.py +544 -0
- package/src/skcapstone/state_diff.py +401 -0
- package/src/skcapstone/summary.py +270 -0
- package/src/skcapstone/sync/backends.py +196 -2
- package/src/skcapstone/sync/engine.py +7 -5
- package/src/skcapstone/sync/models.py +4 -1
- package/src/skcapstone/sync/vault.py +356 -18
- package/src/skcapstone/sync_engine.py +363 -0
- package/src/skcapstone/sync_watcher.py +745 -0
- package/src/skcapstone/systemd.py +331 -0
- package/src/skcapstone/team_comms.py +476 -0
- package/src/skcapstone/team_engine.py +522 -0
- package/src/skcapstone/testrunner.py +300 -0
- package/src/skcapstone/tls.py +150 -0
- package/src/skcapstone/tokens.py +5 -5
- package/src/skcapstone/trust_calibration.py +202 -0
- package/src/skcapstone/trust_graph.py +449 -0
- package/src/skcapstone/trustee_monitor.py +385 -0
- package/src/skcapstone/trustee_ops.py +425 -0
- package/src/skcapstone/unified_search.py +421 -0
- package/src/skcapstone/uninstall_wizard.py +694 -0
- package/src/skcapstone/usage.py +331 -0
- package/src/skcapstone/version_check.py +148 -0
- package/src/skcapstone/warmth_anchor.py +333 -0
- package/src/skcapstone/whoami.py +294 -0
- package/systemd/skcapstone-api.socket +9 -0
- package/systemd/skcapstone-memory-compress.service +18 -0
- package/systemd/skcapstone-memory-compress.timer +11 -0
- package/systemd/skcapstone.service +36 -0
- package/systemd/skcapstone@.service +50 -0
- package/systemd/skcomm-heartbeat.service +18 -0
- package/systemd/skcomm-heartbeat.timer +12 -0
- package/systemd/skcomm-queue-drain.service +17 -0
- package/systemd/skcomm-queue-drain.timer +12 -0
- package/tests/conftest.py +13 -1
- package/tests/integration/__init__.py +1 -0
- package/tests/integration/test_consciousness_e2e.py +877 -0
- package/tests/integration/test_skills_registry.py +744 -0
- package/tests/test_agent_card.py +190 -0
- package/tests/test_agent_runtime.py +1283 -0
- package/tests/test_alerts_cmd.py +291 -0
- package/tests/test_archiver.py +498 -0
- package/tests/test_backup.py +254 -0
- package/tests/test_benchmark.py +366 -0
- package/tests/test_blueprints.py +457 -0
- package/tests/test_capabilities.py +257 -0
- package/tests/test_changelog.py +254 -0
- package/tests/test_chat.py +385 -0
- package/tests/test_claude_md.py +271 -0
- package/tests/test_cli_chat_llm.py +336 -0
- package/tests/test_cli_completions.py +390 -0
- package/tests/test_cli_init_reset.py +164 -0
- package/tests/test_cli_memory.py +208 -0
- package/tests/test_cli_profile.py +294 -0
- package/tests/test_cli_skills.py +223 -0
- package/tests/test_cli_status.py +395 -0
- package/tests/test_cli_test_cmd.py +206 -0
- package/tests/test_cli_test_connection.py +364 -0
- package/tests/test_cloud9_bridge.py +260 -0
- package/tests/test_cloud_provider.py +449 -0
- package/tests/test_cloud_providers.py +522 -0
- package/tests/test_completions.py +158 -0
- package/tests/test_component_manager.py +398 -0
- package/tests/test_config_reload.py +386 -0
- package/tests/test_config_validate.py +529 -0
- package/tests/test_consciousness_e2e.py +296 -0
- package/tests/test_consciousness_loop.py +1289 -0
- package/tests/test_context_loader.py +310 -0
- package/tests/test_conversation_api.py +306 -0
- package/tests/test_conversation_manager.py +381 -0
- package/tests/test_conversation_store.py +391 -0
- package/tests/test_conversation_summarizer.py +302 -0
- package/tests/test_cross_package.py +791 -0
- package/tests/test_crush_shim.py +519 -0
- package/tests/test_daemon.py +781 -0
- package/tests/test_daemon_shutdown.py +309 -0
- package/tests/test_dashboard.py +454 -0
- package/tests/test_discovery.py +200 -6
- package/tests/test_docker_provider.py +966 -0
- package/tests/test_doctor.py +257 -0
- package/tests/test_doctor_fix.py +351 -0
- package/tests/test_e2e_automated.py +292 -0
- package/tests/test_error_queue.py +404 -0
- package/tests/test_export.py +441 -0
- package/tests/test_fallback_tracker.py +219 -0
- package/tests/test_file_transfer.py +397 -0
- package/tests/test_fuse_mount.py +832 -0
- package/tests/test_health_loop.py +422 -0
- package/tests/test_heartbeat.py +354 -0
- package/tests/test_housekeeping.py +195 -0
- package/tests/test_identity_capauth.py +307 -0
- package/tests/test_identity_pillar.py +117 -0
- package/tests/test_install_wizard.py +68 -0
- package/tests/test_integration.py +325 -0
- package/tests/test_kms.py +495 -0
- package/tests/test_llm_providers.py +265 -0
- package/tests/test_local_provider.py +591 -0
- package/tests/test_log_config.py +199 -0
- package/tests/test_logs_cmd.py +287 -0
- package/tests/test_mcp_server.py +1909 -0
- package/tests/test_memory_adapter.py +339 -0
- package/tests/test_memory_curator.py +218 -0
- package/tests/test_memory_engine.py +6 -0
- package/tests/test_memory_fortress.py +571 -0
- package/tests/test_memory_pillar.py +119 -0
- package/tests/test_memory_promoter.py +445 -0
- package/tests/test_memory_verifier.py +420 -0
- package/tests/test_message_crypto.py +187 -0
- package/tests/test_metrics.py +632 -0
- package/tests/test_migrate_memories.py +464 -0
- package/tests/test_model_router.py +546 -0
- package/tests/test_mood.py +394 -0
- package/tests/test_multi_agent.py +269 -0
- package/tests/test_notifications.py +270 -0
- package/tests/test_onboard.py +500 -0
- package/tests/test_peer_directory.py +395 -0
- package/tests/test_peers.py +248 -0
- package/tests/test_pillars.py +87 -9
- package/tests/test_preflight.py +484 -0
- package/tests/test_prompt_adapter.py +331 -0
- package/tests/test_proxmox_provider.py +571 -0
- package/tests/test_pubsub.py +377 -0
- package/tests/test_rate_limiter.py +121 -0
- package/tests/test_registry_client.py +129 -0
- package/tests/test_response_cache.py +312 -0
- package/tests/test_response_scorer.py +294 -0
- package/tests/test_runtime.py +59 -0
- package/tests/test_scheduled_tasks.py +451 -0
- package/tests/test_security.py +250 -0
- package/tests/test_security_pillar.py +213 -0
- package/tests/test_self_healing.py +171 -0
- package/tests/test_session_capture.py +200 -0
- package/tests/test_session_recorder.py +360 -0
- package/tests/test_session_skills.py +235 -0
- package/tests/test_shell.py +210 -0
- package/tests/test_snapshots.py +549 -0
- package/tests/test_soul.py +984 -0
- package/tests/test_soul_swap.py +406 -0
- package/tests/test_spawner.py +211 -0
- package/tests/test_state_diff.py +173 -0
- package/tests/test_summary.py +135 -0
- package/tests/test_sync.py +315 -5
- package/tests/test_sync_backends.py +560 -0
- package/tests/test_sync_engine.py +482 -0
- package/tests/test_sync_pillar.py +344 -0
- package/tests/test_sync_pipeline.py +364 -0
- package/tests/test_sync_vault.py +581 -0
- package/tests/test_syncthing_setup.py +168 -22
- package/tests/test_systemd.py +323 -0
- package/tests/test_team_comms.py +408 -0
- package/tests/test_team_engine.py +397 -0
- package/tests/test_testrunner.py +238 -0
- package/tests/test_trust_calibration.py +204 -0
- package/tests/test_trust_graph.py +207 -0
- package/tests/test_trust_pillar.py +291 -0
- package/tests/test_trustee_cli.py +427 -0
- package/tests/test_trustee_cli_integration.py +325 -0
- package/tests/test_trustee_monitor.py +394 -0
- package/tests/test_trustee_ops.py +355 -0
- package/tests/test_unified_search.py +363 -0
- package/tests/test_uninstall_wizard.py +193 -0
- package/tests/test_usage.py +333 -0
- package/tests/test_version_cmd.py +355 -0
- package/tests/test_warmth_anchor.py +162 -0
- package/tests/test_whoami.py +245 -0
- package/tests/test_ws.py +311 -0
- package/.cursorrules +0 -33
- package/src/skcapstone/cli.py +0 -1441
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Encrypted file transfer — chunked, resumable, sovereign.
|
|
3
|
+
|
|
4
|
+
Files are split into 256 KB chunks, each independently encrypted
|
|
5
|
+
using the agent's KMS-derived service key (Fernet AES-128-CBC).
|
|
6
|
+
Transfers can be paused and resumed by tracking which chunks have
|
|
7
|
+
been sent/received.
|
|
8
|
+
|
|
9
|
+
Architecture:
|
|
10
|
+
Sender:
|
|
11
|
+
1. Split file into 256 KB chunks.
|
|
12
|
+
2. Encrypt each chunk with KMS service key.
|
|
13
|
+
3. Write chunk files to outbox directory.
|
|
14
|
+
4. Create a transfer manifest (JSON) with metadata.
|
|
15
|
+
|
|
16
|
+
Receiver:
|
|
17
|
+
1. Read manifest to learn expected chunks.
|
|
18
|
+
2. Decrypt and verify each chunk (HMAC in Fernet).
|
|
19
|
+
3. Reassemble in order.
|
|
20
|
+
4. Verify final SHA-256 of complete file.
|
|
21
|
+
|
|
22
|
+
Storage layout:
|
|
23
|
+
~/.skcapstone/file-transfer/
|
|
24
|
+
├── outbox/
|
|
25
|
+
│ └── <transfer_id>/
|
|
26
|
+
│ ├── manifest.json
|
|
27
|
+
│ ├── chunk-000.enc
|
|
28
|
+
│ ├── chunk-001.enc
|
|
29
|
+
│ └── ...
|
|
30
|
+
├── inbox/
|
|
31
|
+
│ └── <transfer_id>/
|
|
32
|
+
│ ├── manifest.json
|
|
33
|
+
│ └── ...
|
|
34
|
+
└── completed/
|
|
35
|
+
└── <transfer_id>.json # completion receipt
|
|
36
|
+
|
|
37
|
+
Usage:
|
|
38
|
+
ft = FileTransfer(home)
|
|
39
|
+
ft.initialize()
|
|
40
|
+
transfer = ft.send("/path/to/file.pdf", recipient="lumina")
|
|
41
|
+
# Receiver side:
|
|
42
|
+
assembled = ft.receive(transfer_id)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import hashlib
|
|
48
|
+
import json
|
|
49
|
+
import logging
|
|
50
|
+
import uuid
|
|
51
|
+
from datetime import datetime, timezone
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
from typing import Any, Optional
|
|
54
|
+
|
|
55
|
+
from pydantic import BaseModel, Field
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger("skcapstone.file_transfer")
|
|
58
|
+
|
|
59
|
+
CHUNK_SIZE = 256 * 1024 # 256 KB
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Models
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ChunkInfo(BaseModel):
|
|
68
|
+
"""Metadata for a single file chunk."""
|
|
69
|
+
|
|
70
|
+
index: int
|
|
71
|
+
size: int
|
|
72
|
+
sha256: str
|
|
73
|
+
encrypted: bool = True
|
|
74
|
+
sent: bool = False
|
|
75
|
+
received: bool = False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TransferManifest(BaseModel):
|
|
79
|
+
"""Manifest describing a complete file transfer."""
|
|
80
|
+
|
|
81
|
+
transfer_id: str = Field(default_factory=lambda: uuid.uuid4().hex[:12])
|
|
82
|
+
filename: str
|
|
83
|
+
file_size: int
|
|
84
|
+
file_sha256: str
|
|
85
|
+
chunk_size: int = CHUNK_SIZE
|
|
86
|
+
total_chunks: int
|
|
87
|
+
sender: str = ""
|
|
88
|
+
recipient: str = ""
|
|
89
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
90
|
+
completed_at: Optional[datetime] = None
|
|
91
|
+
chunks: list[ChunkInfo] = Field(default_factory=list)
|
|
92
|
+
encryption_key_label: str = "file-transfer"
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def is_complete(self) -> bool:
|
|
96
|
+
"""Whether all chunks have been sent and received."""
|
|
97
|
+
return all(c.sent for c in self.chunks) and all(c.received for c in self.chunks)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def progress(self) -> float:
|
|
101
|
+
"""Transfer progress as a fraction (0.0 to 1.0)."""
|
|
102
|
+
if not self.chunks:
|
|
103
|
+
return 0.0
|
|
104
|
+
done = sum(1 for c in self.chunks if c.sent or c.received)
|
|
105
|
+
return done / len(self.chunks)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TransferStatus(BaseModel):
|
|
109
|
+
"""Summary of a file transfer."""
|
|
110
|
+
|
|
111
|
+
transfer_id: str
|
|
112
|
+
filename: str
|
|
113
|
+
file_size: int
|
|
114
|
+
direction: str # "send" or "receive"
|
|
115
|
+
progress: float
|
|
116
|
+
total_chunks: int
|
|
117
|
+
chunks_done: int
|
|
118
|
+
sender: str = ""
|
|
119
|
+
recipient: str = ""
|
|
120
|
+
created_at: Optional[datetime] = None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# FileTransfer
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class FileTransfer:
|
|
129
|
+
"""Encrypted chunked file transfer engine.
|
|
130
|
+
|
|
131
|
+
Splits files into 256 KB chunks, encrypts each with a KMS
|
|
132
|
+
service key, and manages transfer state for resumability.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
home: Agent home directory (~/.skcapstone).
|
|
136
|
+
agent_name: Name of the local agent.
|
|
137
|
+
chunk_size: Override chunk size (default 256 KB).
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(
|
|
141
|
+
self,
|
|
142
|
+
home: Path,
|
|
143
|
+
agent_name: str = "anonymous",
|
|
144
|
+
chunk_size: int = CHUNK_SIZE,
|
|
145
|
+
) -> None:
|
|
146
|
+
self._home = home
|
|
147
|
+
self._agent = agent_name
|
|
148
|
+
self._chunk_size = chunk_size
|
|
149
|
+
self._base_dir = home / "file-transfer"
|
|
150
|
+
self._outbox = self._base_dir / "outbox"
|
|
151
|
+
self._inbox = self._base_dir / "inbox"
|
|
152
|
+
self._completed = self._base_dir / "completed"
|
|
153
|
+
|
|
154
|
+
def initialize(self) -> None:
|
|
155
|
+
"""Create the file transfer directory structure."""
|
|
156
|
+
self._outbox.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
self._inbox.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
self._completed.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
|
|
160
|
+
def send(
|
|
161
|
+
self,
|
|
162
|
+
file_path: Path,
|
|
163
|
+
recipient: str,
|
|
164
|
+
encrypt: bool = True,
|
|
165
|
+
) -> TransferManifest:
|
|
166
|
+
"""Prepare a file for transfer by chunking and encrypting.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
file_path: Path to the file to send.
|
|
170
|
+
recipient: Recipient agent name.
|
|
171
|
+
encrypt: Whether to encrypt chunks (default True).
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
TransferManifest with all chunk metadata.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
FileNotFoundError: If the file doesn't exist.
|
|
178
|
+
ValueError: If the file is empty.
|
|
179
|
+
"""
|
|
180
|
+
self.initialize()
|
|
181
|
+
|
|
182
|
+
file_path = Path(file_path)
|
|
183
|
+
if not file_path.exists():
|
|
184
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
185
|
+
|
|
186
|
+
file_data = file_path.read_bytes()
|
|
187
|
+
if not file_data:
|
|
188
|
+
raise ValueError("Cannot transfer empty file")
|
|
189
|
+
|
|
190
|
+
file_hash = hashlib.sha256(file_data).hexdigest()
|
|
191
|
+
total_chunks = (len(file_data) + self._chunk_size - 1) // self._chunk_size
|
|
192
|
+
|
|
193
|
+
manifest = TransferManifest(
|
|
194
|
+
filename=file_path.name,
|
|
195
|
+
file_size=len(file_data),
|
|
196
|
+
file_sha256=file_hash,
|
|
197
|
+
chunk_size=self._chunk_size,
|
|
198
|
+
total_chunks=total_chunks,
|
|
199
|
+
sender=self._agent,
|
|
200
|
+
recipient=recipient,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
transfer_dir = self._outbox / manifest.transfer_id
|
|
204
|
+
transfer_dir.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
|
|
206
|
+
enc_key = self._get_encryption_key() if encrypt else None
|
|
207
|
+
|
|
208
|
+
for i in range(total_chunks):
|
|
209
|
+
start = i * self._chunk_size
|
|
210
|
+
end = min(start + self._chunk_size, len(file_data))
|
|
211
|
+
chunk_data = file_data[start:end]
|
|
212
|
+
chunk_hash = hashlib.sha256(chunk_data).hexdigest()
|
|
213
|
+
|
|
214
|
+
if encrypt and enc_key is not None:
|
|
215
|
+
chunk_data = self._encrypt_chunk(chunk_data, enc_key)
|
|
216
|
+
|
|
217
|
+
chunk_file = transfer_dir / f"chunk-{i:04d}.enc"
|
|
218
|
+
chunk_file.write_bytes(chunk_data)
|
|
219
|
+
|
|
220
|
+
manifest.chunks.append(ChunkInfo(
|
|
221
|
+
index=i,
|
|
222
|
+
size=end - start,
|
|
223
|
+
sha256=chunk_hash,
|
|
224
|
+
encrypted=encrypt and enc_key is not None,
|
|
225
|
+
sent=True,
|
|
226
|
+
))
|
|
227
|
+
|
|
228
|
+
# Write manifest
|
|
229
|
+
manifest_path = transfer_dir / "manifest.json"
|
|
230
|
+
manifest_path.write_text(
|
|
231
|
+
manifest.model_dump_json(indent=2),
|
|
232
|
+
encoding="utf-8",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
logger.info(
|
|
236
|
+
"Prepared transfer %s: %s (%d chunks, %d bytes) -> %s",
|
|
237
|
+
manifest.transfer_id, manifest.filename,
|
|
238
|
+
total_chunks, manifest.file_size, recipient,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
return manifest
|
|
242
|
+
|
|
243
|
+
def receive(
|
|
244
|
+
self,
|
|
245
|
+
transfer_id: str,
|
|
246
|
+
output_dir: Optional[Path] = None,
|
|
247
|
+
) -> Path:
|
|
248
|
+
"""Receive and reassemble a file transfer.
|
|
249
|
+
|
|
250
|
+
Reads chunks from inbox, decrypts them, verifies integrity,
|
|
251
|
+
and reassembles the original file.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
transfer_id: The transfer ID to receive.
|
|
255
|
+
output_dir: Where to write the assembled file.
|
|
256
|
+
Defaults to inbox/<transfer_id>/.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Path to the reassembled file.
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
FileNotFoundError: If manifest not found.
|
|
263
|
+
ValueError: If integrity check fails.
|
|
264
|
+
"""
|
|
265
|
+
# Check both inbox and outbox (for local testing)
|
|
266
|
+
transfer_dir = self._inbox / transfer_id
|
|
267
|
+
if not transfer_dir.is_dir():
|
|
268
|
+
transfer_dir = self._outbox / transfer_id
|
|
269
|
+
if not transfer_dir.is_dir():
|
|
270
|
+
raise FileNotFoundError(f"Transfer {transfer_id} not found")
|
|
271
|
+
|
|
272
|
+
manifest_path = transfer_dir / "manifest.json"
|
|
273
|
+
if not manifest_path.exists():
|
|
274
|
+
raise FileNotFoundError(f"Manifest not found for {transfer_id}")
|
|
275
|
+
|
|
276
|
+
manifest = TransferManifest.model_validate_json(
|
|
277
|
+
manifest_path.read_text(encoding="utf-8")
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
enc_key = self._get_encryption_key()
|
|
281
|
+
assembled = bytearray()
|
|
282
|
+
|
|
283
|
+
for chunk_info in sorted(manifest.chunks, key=lambda c: c.index):
|
|
284
|
+
chunk_file = transfer_dir / f"chunk-{chunk_info.index:04d}.enc"
|
|
285
|
+
if not chunk_file.exists():
|
|
286
|
+
raise FileNotFoundError(
|
|
287
|
+
f"Missing chunk {chunk_info.index} for transfer {transfer_id}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
chunk_data = chunk_file.read_bytes()
|
|
291
|
+
|
|
292
|
+
if chunk_info.encrypted and enc_key is not None:
|
|
293
|
+
chunk_data = self._decrypt_chunk(chunk_data, enc_key)
|
|
294
|
+
|
|
295
|
+
# Verify chunk integrity
|
|
296
|
+
actual_hash = hashlib.sha256(chunk_data).hexdigest()
|
|
297
|
+
if actual_hash != chunk_info.sha256:
|
|
298
|
+
raise ValueError(
|
|
299
|
+
f"Chunk {chunk_info.index} integrity check failed: "
|
|
300
|
+
f"expected {chunk_info.sha256[:16]}..., got {actual_hash[:16]}..."
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
chunk_info.received = True
|
|
304
|
+
assembled.extend(chunk_data)
|
|
305
|
+
|
|
306
|
+
# Verify complete file integrity
|
|
307
|
+
file_hash = hashlib.sha256(assembled).hexdigest()
|
|
308
|
+
if file_hash != manifest.file_sha256:
|
|
309
|
+
raise ValueError(
|
|
310
|
+
f"File integrity check failed: "
|
|
311
|
+
f"expected {manifest.file_sha256[:16]}..., got {file_hash[:16]}..."
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Write assembled file
|
|
315
|
+
dest_dir = output_dir or transfer_dir
|
|
316
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
317
|
+
output_path = dest_dir / manifest.filename
|
|
318
|
+
output_path.write_bytes(assembled)
|
|
319
|
+
|
|
320
|
+
# Record completion
|
|
321
|
+
manifest.completed_at = datetime.now(timezone.utc)
|
|
322
|
+
receipt = self._completed / f"{transfer_id}.json"
|
|
323
|
+
receipt.write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
|
|
324
|
+
|
|
325
|
+
# Update manifest
|
|
326
|
+
manifest_path.write_text(manifest.model_dump_json(indent=2), encoding="utf-8")
|
|
327
|
+
|
|
328
|
+
logger.info(
|
|
329
|
+
"Received transfer %s: %s (%d bytes, %d chunks)",
|
|
330
|
+
transfer_id, manifest.filename, manifest.file_size, manifest.total_chunks,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
return output_path
|
|
334
|
+
|
|
335
|
+
def get_manifest(self, transfer_id: str) -> Optional[TransferManifest]:
|
|
336
|
+
"""Get the manifest for a transfer.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
transfer_id: The transfer ID.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
TransferManifest or None if not found.
|
|
343
|
+
"""
|
|
344
|
+
for base in (self._outbox, self._inbox):
|
|
345
|
+
manifest_path = base / transfer_id / "manifest.json"
|
|
346
|
+
if manifest_path.exists():
|
|
347
|
+
return TransferManifest.model_validate_json(
|
|
348
|
+
manifest_path.read_text(encoding="utf-8")
|
|
349
|
+
)
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
def list_transfers(self, direction: Optional[str] = None) -> list[TransferStatus]:
|
|
353
|
+
"""List all transfers with progress info.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
direction: Filter by "send" or "receive". None = all.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
List of TransferStatus objects.
|
|
360
|
+
"""
|
|
361
|
+
statuses: list[TransferStatus] = []
|
|
362
|
+
|
|
363
|
+
if direction in (None, "send"):
|
|
364
|
+
for d in self._outbox.iterdir() if self._outbox.is_dir() else []:
|
|
365
|
+
if d.is_dir():
|
|
366
|
+
manifest = self._read_manifest(d)
|
|
367
|
+
if manifest:
|
|
368
|
+
statuses.append(self._manifest_to_status(manifest, "send"))
|
|
369
|
+
|
|
370
|
+
if direction in (None, "receive"):
|
|
371
|
+
for d in self._inbox.iterdir() if self._inbox.is_dir() else []:
|
|
372
|
+
if d.is_dir():
|
|
373
|
+
manifest = self._read_manifest(d)
|
|
374
|
+
if manifest:
|
|
375
|
+
statuses.append(self._manifest_to_status(manifest, "receive"))
|
|
376
|
+
|
|
377
|
+
statuses.sort(key=lambda s: s.created_at or datetime.min, reverse=True)
|
|
378
|
+
return statuses
|
|
379
|
+
|
|
380
|
+
def resume_send(self, transfer_id: str) -> list[int]:
|
|
381
|
+
"""Find unsent chunks for a transfer.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
transfer_id: The transfer ID.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
List of chunk indices that haven't been sent.
|
|
388
|
+
"""
|
|
389
|
+
manifest = self.get_manifest(transfer_id)
|
|
390
|
+
if manifest is None:
|
|
391
|
+
return []
|
|
392
|
+
return [c.index for c in manifest.chunks if not c.sent]
|
|
393
|
+
|
|
394
|
+
def resume_receive(self, transfer_id: str) -> list[int]:
|
|
395
|
+
"""Find missing chunks for a transfer.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
transfer_id: The transfer ID.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
List of chunk indices that haven't been received.
|
|
402
|
+
"""
|
|
403
|
+
manifest = self.get_manifest(transfer_id)
|
|
404
|
+
if manifest is None:
|
|
405
|
+
return []
|
|
406
|
+
|
|
407
|
+
transfer_dir = self._inbox / transfer_id
|
|
408
|
+
if not transfer_dir.is_dir():
|
|
409
|
+
transfer_dir = self._outbox / transfer_id
|
|
410
|
+
|
|
411
|
+
missing = []
|
|
412
|
+
for c in manifest.chunks:
|
|
413
|
+
chunk_file = transfer_dir / f"chunk-{c.index:04d}.enc"
|
|
414
|
+
if not chunk_file.exists():
|
|
415
|
+
missing.append(c.index)
|
|
416
|
+
return missing
|
|
417
|
+
|
|
418
|
+
def cleanup(self, transfer_id: str) -> bool:
|
|
419
|
+
"""Remove all files for a completed transfer.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
transfer_id: The transfer ID to clean up.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
True if cleaned up, False if not found.
|
|
426
|
+
"""
|
|
427
|
+
cleaned = False
|
|
428
|
+
for base in (self._outbox, self._inbox):
|
|
429
|
+
transfer_dir = base / transfer_id
|
|
430
|
+
if transfer_dir.is_dir():
|
|
431
|
+
import shutil
|
|
432
|
+
shutil.rmtree(transfer_dir)
|
|
433
|
+
cleaned = True
|
|
434
|
+
return cleaned
|
|
435
|
+
|
|
436
|
+
def status(self) -> dict[str, Any]:
|
|
437
|
+
"""Return file transfer status summary."""
|
|
438
|
+
outbox_count = sum(
|
|
439
|
+
1 for d in self._outbox.iterdir() if d.is_dir()
|
|
440
|
+
) if self._outbox.is_dir() else 0
|
|
441
|
+
inbox_count = sum(
|
|
442
|
+
1 for d in self._inbox.iterdir() if d.is_dir()
|
|
443
|
+
) if self._inbox.is_dir() else 0
|
|
444
|
+
completed_count = sum(
|
|
445
|
+
1 for _ in self._completed.glob("*.json")
|
|
446
|
+
) if self._completed.is_dir() else 0
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
"outbox_transfers": outbox_count,
|
|
450
|
+
"inbox_transfers": inbox_count,
|
|
451
|
+
"completed": completed_count,
|
|
452
|
+
"base_dir": str(self._base_dir),
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# -------------------------------------------------------------------
|
|
456
|
+
# Internal helpers
|
|
457
|
+
# -------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
def _get_encryption_key(self) -> Optional[bytes]:
|
|
460
|
+
"""Get the file transfer encryption key from KMS."""
|
|
461
|
+
try:
|
|
462
|
+
from .kms import KeyStore
|
|
463
|
+
|
|
464
|
+
store = KeyStore(self._home)
|
|
465
|
+
store.initialize()
|
|
466
|
+
key_record = store.derive_service_key("file-transfer")
|
|
467
|
+
return store.get_key_material(key_record.key_id)
|
|
468
|
+
except Exception as exc:
|
|
469
|
+
logger.warning("KMS unavailable for file transfer: %s", exc)
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
def _encrypt_chunk(self, data: bytes, key: bytes) -> bytes:
|
|
473
|
+
"""Encrypt a chunk using Fernet."""
|
|
474
|
+
from .kms import _fernet_encrypt
|
|
475
|
+
|
|
476
|
+
return _fernet_encrypt(data, key)
|
|
477
|
+
|
|
478
|
+
def _decrypt_chunk(self, data: bytes, key: bytes) -> bytes:
|
|
479
|
+
"""Decrypt a chunk using Fernet."""
|
|
480
|
+
from .kms import _fernet_decrypt
|
|
481
|
+
|
|
482
|
+
return _fernet_decrypt(data, key)
|
|
483
|
+
|
|
484
|
+
def _read_manifest(self, transfer_dir: Path) -> Optional[TransferManifest]:
|
|
485
|
+
"""Read a manifest from a transfer directory."""
|
|
486
|
+
manifest_path = transfer_dir / "manifest.json"
|
|
487
|
+
if not manifest_path.exists():
|
|
488
|
+
return None
|
|
489
|
+
try:
|
|
490
|
+
return TransferManifest.model_validate_json(
|
|
491
|
+
manifest_path.read_text(encoding="utf-8")
|
|
492
|
+
)
|
|
493
|
+
except Exception:
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
def _manifest_to_status(
|
|
497
|
+
self, manifest: TransferManifest, direction: str,
|
|
498
|
+
) -> TransferStatus:
|
|
499
|
+
"""Convert a manifest to a status summary."""
|
|
500
|
+
done = sum(1 for c in manifest.chunks if c.sent or c.received)
|
|
501
|
+
return TransferStatus(
|
|
502
|
+
transfer_id=manifest.transfer_id,
|
|
503
|
+
filename=manifest.filename,
|
|
504
|
+
file_size=manifest.file_size,
|
|
505
|
+
direction=direction,
|
|
506
|
+
progress=manifest.progress,
|
|
507
|
+
total_chunks=manifest.total_chunks,
|
|
508
|
+
chunks_done=done,
|
|
509
|
+
sender=manifest.sender,
|
|
510
|
+
recipient=manifest.recipient,
|
|
511
|
+
created_at=manifest.created_at,
|
|
512
|
+
)
|