@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,234 @@
|
|
|
1
|
+
"""Test runner command — unified pytest across all ecosystem packages.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
skcapstone test # run all packages
|
|
5
|
+
skcapstone test --package skcomm # run one package
|
|
6
|
+
skcapstone test --fast # stop on first failure
|
|
7
|
+
skcapstone test --json-out # machine-readable JSON output
|
|
8
|
+
skcapstone test --verbose # pass -v to pytest
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from ._common import console
|
|
20
|
+
from ..testrunner import ECOSYSTEM_PACKAGES, run_all_tests, TestReport, PackageResult
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _monorepo_root() -> Path:
|
|
24
|
+
"""Locate the monorepo root (parent of the skcapstone package dir).
|
|
25
|
+
|
|
26
|
+
Walks up from this file until we find a directory that contains
|
|
27
|
+
the skcapstone sub-package, then returns its parent.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Path to the monorepo root.
|
|
31
|
+
"""
|
|
32
|
+
# src/skcapstone/cli/test_cmd.py → go up 4 levels
|
|
33
|
+
candidate = Path(__file__).resolve().parent.parent.parent.parent.parent
|
|
34
|
+
# candidate is now smilintux-org/skcapstone/src → go one more up to skcapstone pkg root,
|
|
35
|
+
# then one more up to monorepo root
|
|
36
|
+
# Path layout: monorepo/skcapstone/src/skcapstone/cli/test_cmd.py
|
|
37
|
+
# parents[0] = cli/, [1] = skcapstone (pkg), [2] = src/, [3] = skcapstone (repo), [4] = monorepo
|
|
38
|
+
monorepo = Path(__file__).resolve().parents[4]
|
|
39
|
+
if (monorepo / "skcapstone").is_dir():
|
|
40
|
+
return monorepo
|
|
41
|
+
# Fallback: cwd
|
|
42
|
+
return Path.cwd()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _render_table(report: TestReport) -> None:
|
|
46
|
+
"""Print a Rich table summarising per-package results.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
report: Completed test report.
|
|
50
|
+
"""
|
|
51
|
+
from rich.table import Table
|
|
52
|
+
from rich.panel import Panel
|
|
53
|
+
|
|
54
|
+
table = Table(title="Ecosystem Test Results", show_lines=False, expand=True)
|
|
55
|
+
table.add_column("Package", style="bold", min_width=16)
|
|
56
|
+
table.add_column("Status", min_width=10)
|
|
57
|
+
table.add_column("Passed", justify="right", min_width=7)
|
|
58
|
+
table.add_column("Failed", justify="right", min_width=7)
|
|
59
|
+
table.add_column("Errors", justify="right", min_width=7)
|
|
60
|
+
table.add_column("Skipped", justify="right", min_width=7)
|
|
61
|
+
table.add_column("Duration", justify="right", min_width=9)
|
|
62
|
+
|
|
63
|
+
for r in report.results:
|
|
64
|
+
if not r.available:
|
|
65
|
+
table.add_row(
|
|
66
|
+
r.name,
|
|
67
|
+
"[dim]N/A[/]",
|
|
68
|
+
"-", "-", "-", "-",
|
|
69
|
+
"[dim]—[/]",
|
|
70
|
+
)
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
if r.success:
|
|
74
|
+
status = "[bold green]PASS[/]"
|
|
75
|
+
else:
|
|
76
|
+
status = "[bold red]FAIL[/]"
|
|
77
|
+
|
|
78
|
+
failed_str = f"[red]{r.failed}[/]" if r.failed else "0"
|
|
79
|
+
errors_str = f"[red]{r.errors}[/]" if r.errors else "0"
|
|
80
|
+
skipped_str = f"[yellow]{r.skipped}[/]" if r.skipped else "0"
|
|
81
|
+
|
|
82
|
+
table.add_row(
|
|
83
|
+
r.name,
|
|
84
|
+
status,
|
|
85
|
+
str(r.passed),
|
|
86
|
+
failed_str,
|
|
87
|
+
errors_str,
|
|
88
|
+
skipped_str,
|
|
89
|
+
f"{r.duration_s:.1f}s",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
console.print()
|
|
93
|
+
console.print(table)
|
|
94
|
+
|
|
95
|
+
# Footer summary
|
|
96
|
+
total_pass = report.total_passed
|
|
97
|
+
total_fail = report.total_failed
|
|
98
|
+
total_err = report.total_errors
|
|
99
|
+
pkgs_tested = report.packages_tested
|
|
100
|
+
overall_duration = f"{report.duration_s:.1f}s"
|
|
101
|
+
|
|
102
|
+
if report.all_passed:
|
|
103
|
+
summary_color = "green"
|
|
104
|
+
verdict = "ALL PASSED"
|
|
105
|
+
else:
|
|
106
|
+
summary_color = "red"
|
|
107
|
+
verdict = "FAILURES DETECTED"
|
|
108
|
+
|
|
109
|
+
console.print(
|
|
110
|
+
Panel(
|
|
111
|
+
f"[bold {summary_color}]{verdict}[/] "
|
|
112
|
+
f"[green]{total_pass} passed[/] "
|
|
113
|
+
+ (f"[red]{total_fail} failed[/] " if total_fail else "")
|
|
114
|
+
+ (f"[red]{total_err} errors[/] " if total_err else "")
|
|
115
|
+
+ f"across {pkgs_tested} package(s) "
|
|
116
|
+
f"in {overall_duration}",
|
|
117
|
+
border_style=summary_color,
|
|
118
|
+
expand=False,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
console.print()
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _render_failures(report: TestReport) -> None:
|
|
125
|
+
"""Print abbreviated pytest output for any failed package.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
report: Completed test report.
|
|
129
|
+
"""
|
|
130
|
+
for r in report.results:
|
|
131
|
+
if r.available and not r.success and r.output.strip():
|
|
132
|
+
console.print(f"\n[bold red]── {r.name} output ──[/]\n")
|
|
133
|
+
console.print(r.output)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def register_test_commands(main: click.Group) -> None:
|
|
137
|
+
"""Register the `skcapstone test` command.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
main: Root Click group.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
@main.command("test")
|
|
144
|
+
@click.option(
|
|
145
|
+
"--package", "-p",
|
|
146
|
+
"packages",
|
|
147
|
+
multiple=True,
|
|
148
|
+
metavar="NAME",
|
|
149
|
+
help="Restrict to one or more packages (repeat for multiple).",
|
|
150
|
+
)
|
|
151
|
+
@click.option(
|
|
152
|
+
"--fast", is_flag=True,
|
|
153
|
+
help="Stop after the first failing package.",
|
|
154
|
+
)
|
|
155
|
+
@click.option(
|
|
156
|
+
"--verbose", "-v", is_flag=True,
|
|
157
|
+
help="Pass -v to pytest for detailed output.",
|
|
158
|
+
)
|
|
159
|
+
@click.option(
|
|
160
|
+
"--json-out", is_flag=True,
|
|
161
|
+
help="Emit machine-readable JSON instead of a Rich table.",
|
|
162
|
+
)
|
|
163
|
+
@click.option(
|
|
164
|
+
"--timeout", default=120, show_default=True, type=int,
|
|
165
|
+
help="Per-package timeout in seconds.",
|
|
166
|
+
)
|
|
167
|
+
@click.option(
|
|
168
|
+
"--root", default=None, type=click.Path(),
|
|
169
|
+
help="Override monorepo root path (auto-detected by default).",
|
|
170
|
+
)
|
|
171
|
+
@click.option(
|
|
172
|
+
"--show-output", is_flag=True,
|
|
173
|
+
help="Print pytest output for failing packages.",
|
|
174
|
+
)
|
|
175
|
+
def test_cmd(
|
|
176
|
+
packages: tuple[str, ...],
|
|
177
|
+
fast: bool,
|
|
178
|
+
verbose: bool,
|
|
179
|
+
json_out: bool,
|
|
180
|
+
timeout: int,
|
|
181
|
+
root: str | None,
|
|
182
|
+
show_output: bool,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Run pytest across all ecosystem packages and show a summary table.
|
|
185
|
+
|
|
186
|
+
Discovers packages in the monorepo (skcapstone, capauth, skcomm,
|
|
187
|
+
skchat, skmemory, cloud9-python) and runs their test suites in
|
|
188
|
+
sequence, then renders a consolidated Rich table.
|
|
189
|
+
|
|
190
|
+
\b
|
|
191
|
+
Examples:
|
|
192
|
+
skcapstone test
|
|
193
|
+
skcapstone test --package skcomm
|
|
194
|
+
skcapstone test -p skcomm -p skchat
|
|
195
|
+
skcapstone test --fast --verbose
|
|
196
|
+
skcapstone test --json-out | jq .
|
|
197
|
+
"""
|
|
198
|
+
monorepo = Path(root).expanduser() if root else _monorepo_root()
|
|
199
|
+
|
|
200
|
+
pkg_filter = list(packages) if packages else None
|
|
201
|
+
|
|
202
|
+
valid_names = {p["name"] for p in ECOSYSTEM_PACKAGES}
|
|
203
|
+
if pkg_filter:
|
|
204
|
+
invalid = [n for n in pkg_filter if n not in valid_names]
|
|
205
|
+
if invalid:
|
|
206
|
+
console.print(
|
|
207
|
+
f"[red]Unknown package(s): {', '.join(invalid)}[/]\n"
|
|
208
|
+
f"Valid: {', '.join(sorted(valid_names))}"
|
|
209
|
+
)
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
|
|
212
|
+
if not json_out:
|
|
213
|
+
names_str = ", ".join(pkg_filter) if pkg_filter else "all packages"
|
|
214
|
+
console.print(f"\n [cyan]Running tests for:[/] {names_str}")
|
|
215
|
+
console.print(f" [dim]Monorepo root: {monorepo}[/]\n")
|
|
216
|
+
|
|
217
|
+
report = run_all_tests(
|
|
218
|
+
monorepo_root=monorepo,
|
|
219
|
+
packages=pkg_filter,
|
|
220
|
+
fail_fast=fast,
|
|
221
|
+
verbose=verbose,
|
|
222
|
+
timeout=timeout,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if json_out:
|
|
226
|
+
click.echo(json.dumps(report.to_dict(), indent=2))
|
|
227
|
+
sys.exit(0 if report.all_passed else 1)
|
|
228
|
+
|
|
229
|
+
_render_table(report)
|
|
230
|
+
|
|
231
|
+
if show_output:
|
|
232
|
+
_render_failures(report)
|
|
233
|
+
|
|
234
|
+
sys.exit(0 if report.all_passed else 1)
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""test-connection command — ping a peer via SKComm and measure latency.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
skcapstone test-connection <peer>
|
|
5
|
+
skcapstone test-connection <peer> --timeout 5
|
|
6
|
+
skcapstone test-connection <peer> --count 3
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
|
|
18
|
+
from ._common import AGENT_HOME, console
|
|
19
|
+
from ..chat import AgentChat
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Payload type markers
|
|
23
|
+
_PING_KEY = "skchat_ping"
|
|
24
|
+
_PONG_KEY = "skchat_pong"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make_ping_payload(nonce: str, sender: str) -> str:
|
|
28
|
+
"""Serialize a ping message.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
nonce: Unique identifier for this ping/pong round-trip.
|
|
32
|
+
sender: Sending agent name.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
str: JSON payload string.
|
|
36
|
+
"""
|
|
37
|
+
return json.dumps({_PING_KEY: True, "nonce": nonce, "sender": sender})
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _is_pong_for(payload: str, nonce: str) -> bool:
|
|
41
|
+
"""Return True if *payload* is a pong matching *nonce*.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
payload: Raw message content string.
|
|
45
|
+
nonce: The nonce sent in the corresponding ping.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
bool: True when the payload is a valid matching pong.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
data = json.loads(payload)
|
|
52
|
+
return bool(data.get(_PONG_KEY)) and data.get("nonce") == nonce
|
|
53
|
+
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_ping(payload: str) -> tuple[bool, str, str]:
|
|
58
|
+
"""Parse an incoming ping payload.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
payload: Raw message content string.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
tuple: (is_ping, nonce, sender) — nonce/sender are empty strings
|
|
65
|
+
when the payload is not a ping.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
data = json.loads(payload)
|
|
69
|
+
if data.get(_PING_KEY):
|
|
70
|
+
return True, data.get("nonce", ""), data.get("sender", "")
|
|
71
|
+
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
72
|
+
pass
|
|
73
|
+
return False, "", ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _make_pong_payload(nonce: str, sender: str) -> str:
|
|
77
|
+
"""Serialize a pong reply.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
nonce: The nonce received from the ping.
|
|
81
|
+
sender: Replying agent name.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
str: JSON payload string.
|
|
85
|
+
"""
|
|
86
|
+
return json.dumps({_PONG_KEY: True, "nonce": nonce, "sender": sender})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def ping_peer(
|
|
90
|
+
peer: str,
|
|
91
|
+
home: Path,
|
|
92
|
+
identity: str,
|
|
93
|
+
timeout: float = 10.0,
|
|
94
|
+
) -> dict:
|
|
95
|
+
"""Send a ping to *peer* and wait for a matching pong.
|
|
96
|
+
|
|
97
|
+
Sends a structured ping message via AgentChat and polls the inbox
|
|
98
|
+
for a pong response with a matching nonce. Times out after *timeout*
|
|
99
|
+
seconds.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
peer: Peer agent name or identity.
|
|
103
|
+
home: Agent home directory.
|
|
104
|
+
identity: Local agent name used as sender.
|
|
105
|
+
timeout: Maximum seconds to wait for a pong.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
dict with keys:
|
|
109
|
+
- ``reachable`` (bool): True if pong was received in time.
|
|
110
|
+
- ``latency_ms`` (float | None): Round-trip time in milliseconds,
|
|
111
|
+
or None on timeout.
|
|
112
|
+
- ``transport`` (str | None): Transport that delivered the ping.
|
|
113
|
+
- ``error`` (str | None): Error message, if any.
|
|
114
|
+
"""
|
|
115
|
+
result: dict = {
|
|
116
|
+
"reachable": False,
|
|
117
|
+
"latency_ms": None,
|
|
118
|
+
"transport": None,
|
|
119
|
+
"error": None,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
nonce = str(uuid.uuid4())
|
|
123
|
+
agent_chat = AgentChat(home=home, identity=identity)
|
|
124
|
+
|
|
125
|
+
# Send the ping
|
|
126
|
+
t0 = time.monotonic()
|
|
127
|
+
send_result = agent_chat.send(peer, _make_ping_payload(nonce, identity))
|
|
128
|
+
|
|
129
|
+
if not send_result.get("delivered") and not send_result.get("stored"):
|
|
130
|
+
result["error"] = send_result.get("error") or "send failed (no transport)"
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
result["transport"] = send_result.get("transport")
|
|
134
|
+
|
|
135
|
+
# If we only stored locally (no live transport), report accordingly
|
|
136
|
+
if not send_result.get("delivered"):
|
|
137
|
+
result["error"] = "ping stored locally — no live transport available"
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
# Poll for pong
|
|
141
|
+
deadline = t0 + timeout
|
|
142
|
+
poll_interval = 0.25 # seconds
|
|
143
|
+
|
|
144
|
+
while time.monotonic() < deadline:
|
|
145
|
+
messages = agent_chat.receive(limit=20)
|
|
146
|
+
for msg in messages:
|
|
147
|
+
content = msg.get("content", "")
|
|
148
|
+
sender_name = msg.get("sender", "")
|
|
149
|
+
if sender_name == peer and _is_pong_for(content, nonce):
|
|
150
|
+
elapsed_ms = (time.monotonic() - t0) * 1000.0
|
|
151
|
+
result["reachable"] = True
|
|
152
|
+
result["latency_ms"] = round(elapsed_ms, 2)
|
|
153
|
+
return result
|
|
154
|
+
time.sleep(poll_interval)
|
|
155
|
+
|
|
156
|
+
result["error"] = f"timeout after {timeout:.0f}s — no pong received"
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def register_test_connection_commands(main: click.Group) -> None:
|
|
161
|
+
"""Register the test-connection command on *main*."""
|
|
162
|
+
|
|
163
|
+
@main.command("test-connection")
|
|
164
|
+
@click.argument("peer")
|
|
165
|
+
@click.option(
|
|
166
|
+
"--timeout", "-t",
|
|
167
|
+
default=10.0,
|
|
168
|
+
show_default=True,
|
|
169
|
+
help="Seconds to wait for a pong response.",
|
|
170
|
+
)
|
|
171
|
+
@click.option(
|
|
172
|
+
"--count", "-c",
|
|
173
|
+
default=1,
|
|
174
|
+
show_default=True,
|
|
175
|
+
help="Number of pings to send (reports min/avg/max when > 1).",
|
|
176
|
+
)
|
|
177
|
+
@click.option("--home", default=AGENT_HOME, type=click.Path())
|
|
178
|
+
def test_connection(peer: str, timeout: float, count: int, home: str) -> None:
|
|
179
|
+
"""Test connectivity to a peer by sending a ping via SKComm.
|
|
180
|
+
|
|
181
|
+
Sends a ping message, waits for the peer to reply with a pong,
|
|
182
|
+
and reports the round-trip latency. Exits with code 1 when the
|
|
183
|
+
peer is unreachable.
|
|
184
|
+
|
|
185
|
+
\b
|
|
186
|
+
Examples:
|
|
187
|
+
skcapstone test-connection lumina
|
|
188
|
+
skcapstone test-connection opus --timeout 5
|
|
189
|
+
skcapstone test-connection jarvis --count 3
|
|
190
|
+
"""
|
|
191
|
+
from ..runtime import get_runtime
|
|
192
|
+
|
|
193
|
+
home_path = Path(home).expanduser()
|
|
194
|
+
try:
|
|
195
|
+
runtime = get_runtime(home_path)
|
|
196
|
+
identity = runtime.manifest.name or "unknown"
|
|
197
|
+
except Exception:
|
|
198
|
+
identity = "unknown"
|
|
199
|
+
|
|
200
|
+
console.print()
|
|
201
|
+
console.print(
|
|
202
|
+
f" PING [cyan]{peer}[/] (timeout={timeout:.0f}s, count={count})"
|
|
203
|
+
)
|
|
204
|
+
console.print()
|
|
205
|
+
|
|
206
|
+
latencies: list[float] = []
|
|
207
|
+
last_error: str | None = None
|
|
208
|
+
last_transport: str | None = None
|
|
209
|
+
|
|
210
|
+
for i in range(count):
|
|
211
|
+
result = ping_peer(peer, home_path, identity, timeout=timeout)
|
|
212
|
+
last_transport = result.get("transport") or last_transport
|
|
213
|
+
|
|
214
|
+
if result["reachable"]:
|
|
215
|
+
ms = result["latency_ms"]
|
|
216
|
+
latencies.append(ms)
|
|
217
|
+
console.print(
|
|
218
|
+
f" [{i + 1}/{count}] [green]pong from {peer}[/] "
|
|
219
|
+
f"latency=[bold]{ms:.1f} ms[/]"
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
last_error = result.get("error") or "unreachable"
|
|
223
|
+
console.print(
|
|
224
|
+
f" [{i + 1}/{count}] [red]no pong[/] {last_error}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
console.print()
|
|
228
|
+
|
|
229
|
+
if latencies:
|
|
230
|
+
if count > 1:
|
|
231
|
+
lo = min(latencies)
|
|
232
|
+
hi = max(latencies)
|
|
233
|
+
avg = sum(latencies) / len(latencies)
|
|
234
|
+
console.print(
|
|
235
|
+
f" [bold green]REACHABLE[/] "
|
|
236
|
+
f"min={lo:.1f}ms avg={avg:.1f}ms max={hi:.1f}ms "
|
|
237
|
+
f"({len(latencies)}/{count} received)"
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
console.print(
|
|
241
|
+
f" [bold green]REACHABLE[/] "
|
|
242
|
+
f"latency={latencies[0]:.1f} ms"
|
|
243
|
+
)
|
|
244
|
+
if last_transport:
|
|
245
|
+
console.print(f" transport: [dim]{last_transport}[/]")
|
|
246
|
+
else:
|
|
247
|
+
console.print(
|
|
248
|
+
f" [bold red]UNREACHABLE[/] {last_error or 'no pong received'}"
|
|
249
|
+
)
|
|
250
|
+
console.print()
|
|
251
|
+
raise SystemExit(1)
|
|
252
|
+
|
|
253
|
+
console.print()
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Capability token commands: issue, list, verify, revoke, export."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
|
|
10
|
+
from ._common import AGENT_HOME, console
|
|
11
|
+
from ._validators import validate_agent_name, validate_task_id
|
|
12
|
+
from ..pillars.security import audit_event
|
|
13
|
+
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def register_token_commands(main: click.Group) -> None:
|
|
18
|
+
"""Register the token command group."""
|
|
19
|
+
|
|
20
|
+
@main.group()
|
|
21
|
+
def token():
|
|
22
|
+
"""Manage capability tokens.
|
|
23
|
+
|
|
24
|
+
Issue, verify, list, and revoke PGP-signed capability
|
|
25
|
+
tokens for fine-grained agent authorization.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@token.command("issue")
|
|
29
|
+
@click.option("--home", default=AGENT_HOME, type=click.Path())
|
|
30
|
+
@click.option("--subject", required=True, help="Who the token is for.")
|
|
31
|
+
@click.option("--cap", multiple=True, required=True, help="Capabilities to grant.")
|
|
32
|
+
@click.option("--ttl", default=24, help="Hours until expiry (0 = no expiry).")
|
|
33
|
+
@click.option("--type", "token_type", default="capability", help="Token type.")
|
|
34
|
+
@click.option("--no-sign", is_flag=True, help="Skip PGP signing.")
|
|
35
|
+
def token_issue(home, subject, cap, ttl, token_type, no_sign):
|
|
36
|
+
"""Issue a new capability token."""
|
|
37
|
+
from ..tokens import TokenType, issue_token
|
|
38
|
+
|
|
39
|
+
validate_agent_name(subject)
|
|
40
|
+
|
|
41
|
+
home_path = Path(home).expanduser()
|
|
42
|
+
if not home_path.exists():
|
|
43
|
+
console.print("[bold red]No agent found.[/] Run skcapstone init first.")
|
|
44
|
+
sys.exit(1)
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
tt = TokenType(token_type)
|
|
48
|
+
except ValueError:
|
|
49
|
+
console.print(f"[red]Invalid token type:[/] {token_type}")
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
ttl_hours = ttl if ttl > 0 else None
|
|
53
|
+
capabilities = list(cap)
|
|
54
|
+
|
|
55
|
+
console.print(f"\n Issuing [cyan]{tt.value}[/] token for [bold]{subject}[/]...")
|
|
56
|
+
signed = issue_token(
|
|
57
|
+
home=home_path, subject=subject, capabilities=capabilities,
|
|
58
|
+
token_type=tt, ttl_hours=ttl_hours, sign=not no_sign,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
console.print(f" [green]Token issued:[/] {signed.payload.token_id[:16]}...")
|
|
62
|
+
console.print(f" Capabilities: {', '.join(capabilities)}")
|
|
63
|
+
if signed.payload.expires_at:
|
|
64
|
+
console.print(f" Expires: {signed.payload.expires_at.isoformat()}")
|
|
65
|
+
else:
|
|
66
|
+
console.print(" Expires: [yellow]never[/]")
|
|
67
|
+
if signed.signature:
|
|
68
|
+
console.print(" [green]PGP signed[/]")
|
|
69
|
+
else:
|
|
70
|
+
console.print(" [yellow]Unsigned[/]")
|
|
71
|
+
|
|
72
|
+
audit_event(home_path, "TOKEN_ISSUE", f"Token {signed.payload.token_id[:16]} for {subject}")
|
|
73
|
+
console.print()
|
|
74
|
+
|
|
75
|
+
@token.command("list")
|
|
76
|
+
@click.option("--home", default=AGENT_HOME, type=click.Path())
|
|
77
|
+
def token_list(home):
|
|
78
|
+
"""List all issued tokens."""
|
|
79
|
+
from ..tokens import is_revoked, list_tokens
|
|
80
|
+
|
|
81
|
+
home_path = Path(home).expanduser()
|
|
82
|
+
if not home_path.exists():
|
|
83
|
+
console.print("[bold red]No agent found.[/]")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
tokens = list_tokens(home_path)
|
|
87
|
+
if not tokens:
|
|
88
|
+
console.print("\n [dim]No tokens issued yet.[/]\n")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
table = Table(title="Capability Tokens", show_lines=True)
|
|
92
|
+
table.add_column("ID", style="cyan", max_width=16)
|
|
93
|
+
table.add_column("Type", style="bold")
|
|
94
|
+
table.add_column("Subject")
|
|
95
|
+
table.add_column("Capabilities")
|
|
96
|
+
table.add_column("Status")
|
|
97
|
+
table.add_column("Expires")
|
|
98
|
+
|
|
99
|
+
for t in tokens:
|
|
100
|
+
p = t.payload
|
|
101
|
+
revoked = is_revoked(home_path, p.token_id)
|
|
102
|
+
if revoked:
|
|
103
|
+
st = "[red]REVOKED[/]"
|
|
104
|
+
elif p.is_expired:
|
|
105
|
+
st = "[yellow]EXPIRED[/]"
|
|
106
|
+
elif t.signature:
|
|
107
|
+
st = "[green]SIGNED[/]"
|
|
108
|
+
else:
|
|
109
|
+
st = "[dim]UNSIGNED[/]"
|
|
110
|
+
|
|
111
|
+
exp_str = p.expires_at.strftime("%m/%d %H:%M") if p.expires_at else "never"
|
|
112
|
+
table.add_row(p.token_id[:16], p.token_type.value, p.subject,
|
|
113
|
+
", ".join(p.capabilities), st, exp_str)
|
|
114
|
+
|
|
115
|
+
console.print()
|
|
116
|
+
console.print(table)
|
|
117
|
+
console.print()
|
|
118
|
+
|
|
119
|
+
@token.command("verify")
|
|
120
|
+
@click.argument("token_id")
|
|
121
|
+
@click.option("--home", default=AGENT_HOME, type=click.Path())
|
|
122
|
+
def token_verify(token_id, home):
|
|
123
|
+
"""Verify a token's signature and validity."""
|
|
124
|
+
from ..tokens import is_revoked, list_tokens, verify_token
|
|
125
|
+
|
|
126
|
+
validate_task_id(token_id) # token IDs are hex UUIDs
|
|
127
|
+
|
|
128
|
+
home_path = Path(home).expanduser()
|
|
129
|
+
tokens = list_tokens(home_path)
|
|
130
|
+
|
|
131
|
+
target = None
|
|
132
|
+
for t in tokens:
|
|
133
|
+
if t.payload.token_id.startswith(token_id):
|
|
134
|
+
target = t
|
|
135
|
+
break
|
|
136
|
+
|
|
137
|
+
if not target:
|
|
138
|
+
console.print(f"[red]Token not found:[/] {token_id}")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
if is_revoked(home_path, target.payload.token_id):
|
|
142
|
+
console.print(f"\n [red]REVOKED[/] Token {token_id[:16]} has been revoked.\n")
|
|
143
|
+
sys.exit(1)
|
|
144
|
+
|
|
145
|
+
valid = verify_token(target, home_path)
|
|
146
|
+
if valid:
|
|
147
|
+
console.print(f"\n [green]VALID[/] Token {token_id[:16]}")
|
|
148
|
+
console.print(f" Subject: {target.payload.subject}")
|
|
149
|
+
console.print(f" Capabilities: {', '.join(target.payload.capabilities)}")
|
|
150
|
+
else:
|
|
151
|
+
console.print(f"\n [red]INVALID[/] Token {token_id[:16]}")
|
|
152
|
+
if target.payload.is_expired:
|
|
153
|
+
console.print(" Reason: expired")
|
|
154
|
+
else:
|
|
155
|
+
console.print(" Reason: signature verification failed")
|
|
156
|
+
console.print()
|
|
157
|
+
|
|
158
|
+
@token.command("revoke")
|
|
159
|
+
@click.argument("token_id")
|
|
160
|
+
@click.option("--home", default=AGENT_HOME, type=click.Path())
|
|
161
|
+
def token_revoke(token_id, home):
|
|
162
|
+
"""Revoke a previously issued token."""
|
|
163
|
+
from ..tokens import list_tokens, revoke_token
|
|
164
|
+
|
|
165
|
+
validate_task_id(token_id) # token IDs are hex UUIDs
|
|
166
|
+
|
|
167
|
+
home_path = Path(home).expanduser()
|
|
168
|
+
tokens = list_tokens(home_path)
|
|
169
|
+
|
|
170
|
+
full_id = None
|
|
171
|
+
for t in tokens:
|
|
172
|
+
if t.payload.token_id.startswith(token_id):
|
|
173
|
+
full_id = t.payload.token_id
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
if not full_id:
|
|
177
|
+
console.print(f"[red]Token not found:[/] {token_id}")
|
|
178
|
+
sys.exit(1)
|
|
179
|
+
|
|
180
|
+
revoke_token(home_path, full_id)
|
|
181
|
+
console.print(f"\n [red]REVOKED[/] Token {token_id[:16]}...")
|
|
182
|
+
audit_event(home_path, "TOKEN_REVOKE", f"Token {token_id[:16]} revoked")
|
|
183
|
+
console.print()
|
|
184
|
+
|
|
185
|
+
@token.command("export")
|
|
186
|
+
@click.argument("token_id")
|
|
187
|
+
@click.option("--home", default=AGENT_HOME, type=click.Path())
|
|
188
|
+
def token_export(token_id, home):
|
|
189
|
+
"""Export a token as portable JSON."""
|
|
190
|
+
from ..tokens import export_token, list_tokens
|
|
191
|
+
|
|
192
|
+
validate_task_id(token_id) # token IDs are hex UUIDs
|
|
193
|
+
|
|
194
|
+
home_path = Path(home).expanduser()
|
|
195
|
+
tokens = list_tokens(home_path)
|
|
196
|
+
|
|
197
|
+
target = None
|
|
198
|
+
for t in tokens:
|
|
199
|
+
if t.payload.token_id.startswith(token_id):
|
|
200
|
+
target = t
|
|
201
|
+
break
|
|
202
|
+
|
|
203
|
+
if not target:
|
|
204
|
+
console.print(f"[red]Token not found:[/] {token_id}")
|
|
205
|
+
sys.exit(1)
|
|
206
|
+
|
|
207
|
+
console.print(export_token(target))
|