polyrob 0.5.0__py3-none-any.whl
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.
- agents/README.md +453 -0
- agents/__init__.py +164 -0
- agents/base_agent.py +743 -0
- agents/personality/character.py +119 -0
- agents/personality/character_manager.py +309 -0
- agents/personality/characters/rob.character.json +56 -0
- agents/personality/characters/trump.character.json +63 -0
- agents/personality/persona_render.py +78 -0
- agents/personality/persona_resolver.py +115 -0
- agents/prompt/__init__.py +58 -0
- agents/prompt/base_prompt.py +92 -0
- agents/prompt/system.py +417 -0
- agents/task/README_PATH_MANAGEMENT.md +141 -0
- agents/task/__init__.py +162 -0
- agents/task/agent/__init__.py +70 -0
- agents/task/agent/agent_state.py +408 -0
- agents/task/agent/async_delegation.py +274 -0
- agents/task/agent/autonomy_state.py +275 -0
- agents/task/agent/conversation.py +75 -0
- agents/task/agent/core/__init__.py +6 -0
- agents/task/agent/core/aux_metering.py +61 -0
- agents/task/agent/core/background_review.py +157 -0
- agents/task/agent/core/construction.py +1179 -0
- agents/task/agent/core/conversational_exit.py +49 -0
- agents/task/agent/core/correspondent_gate.py +227 -0
- agents/task/agent/core/curator.py +246 -0
- agents/task/agent/core/episodic_digest.py +103 -0
- agents/task/agent/core/error_recovery.py +480 -0
- agents/task/agent/core/history_io.py +257 -0
- agents/task/agent/core/llm_provisioning.py +287 -0
- agents/task/agent/core/llm_runner.py +547 -0
- agents/task/agent/core/logging_io.py +276 -0
- agents/task/agent/core/loop_detection.py +262 -0
- agents/task/agent/core/memory_prefetch.py +325 -0
- agents/task/agent/core/memory_writer.py +667 -0
- agents/task/agent/core/model_introspection.py +82 -0
- agents/task/agent/core/model_swap.py +177 -0
- agents/task/agent/core/next_action_internal.py +1331 -0
- agents/task/agent/core/output_validation.py +303 -0
- agents/task/agent/core/project_context.py +272 -0
- agents/task/agent/core/resources.py +93 -0
- agents/task/agent/core/result_processing.py +389 -0
- agents/task/agent/core/run_loop.py +661 -0
- agents/task/agent/core/safety_lifecycle.py +228 -0
- agents/task/agent/core/secret_guard.py +293 -0
- agents/task/agent/core/self_wake.py +280 -0
- agents/task/agent/core/session_metadata.py +104 -0
- agents/task/agent/core/step.py +781 -0
- agents/task/agent/core/step_execution.py +398 -0
- agents/task/agent/core/step_telemetry.py +222 -0
- agents/task/agent/core/test_aux_metering.py +50 -0
- agents/task/agent/core/test_billing_failover.py +68 -0
- agents/task/agent/core/test_fatal_error_classifier.py +38 -0
- agents/task/agent/core/test_judge_validation.py +275 -0
- agents/task/agent/core/test_model_swap.py +190 -0
- agents/task/agent/core/test_reflection_offload.py +54 -0
- agents/task/agent/core/turn_input.py +8 -0
- agents/task/agent/core/untrusted_wrap.py +103 -0
- agents/task/agent/core/user_ingress.py +228 -0
- agents/task/agent/hitl_manager.py +396 -0
- agents/task/agent/log_sanitize.py +36 -0
- agents/task/agent/message_manager/config.py +97 -0
- agents/task/agent/message_manager/service.py +758 -0
- agents/task/agent/message_manager/tests.py +305 -0
- agents/task/agent/message_manager/tool_call_builder.py +539 -0
- agents/task/agent/message_manager/tool_message_repair.py +364 -0
- agents/task/agent/message_manager/views.py +143 -0
- agents/task/agent/messages/__init__.py +8 -0
- agents/task/agent/messages/builders.py +309 -0
- agents/task/agent/messages/compactor.py +611 -0
- agents/task/agent/messages/context_references.py +271 -0
- agents/task/agent/messages/filters.py +333 -0
- agents/task/agent/messages/guidance.py +328 -0
- agents/task/agent/messages/persistence.py +448 -0
- agents/task/agent/messages/retrieval.py +506 -0
- agents/task/agent/messages/sqlite_persistence.py +85 -0
- agents/task/agent/messages/test_compaction_upgrade.py +349 -0
- agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
- agents/task/agent/messages/test_sqlite_persistence.py +54 -0
- agents/task/agent/messages/token_counter.py +532 -0
- agents/task/agent/orchestrator.py +776 -0
- agents/task/agent/profile_manager.py +105 -0
- agents/task/agent/profile_registry.py +173 -0
- agents/task/agent/prompts.py +1090 -0
- agents/task/agent/scenario_registry.py +226 -0
- agents/task/agent/service.py +509 -0
- agents/task/agent/session.py +984 -0
- agents/task/agent/skill_discovery.py +110 -0
- agents/task/agent/skill_frontmatter.py +94 -0
- agents/task/agent/skill_manager.py +1201 -0
- agents/task/agent/skill_store.py +627 -0
- agents/task/agent/skill_validation.py +56 -0
- agents/task/agent/skill_writer.py +492 -0
- agents/task/agent/sub_agent_manager.py +1137 -0
- agents/task/agent/test_session_disk_recovery.py +34 -0
- agents/task/agent/tests.py +513 -0
- agents/task/agent/tool_call_tracker.py +511 -0
- agents/task/agent/views.py +469 -0
- agents/task/config.py +432 -0
- agents/task/constants.py +1384 -0
- agents/task/flag_defaults.py +63 -0
- agents/task/goals/__init__.py +1 -0
- agents/task/goals/autonomy_marker.py +28 -0
- agents/task/goals/board.py +690 -0
- agents/task/goals/completion_judge.py +247 -0
- agents/task/goals/context.py +62 -0
- agents/task/goals/dispatcher.py +848 -0
- agents/task/goals/escalation.py +102 -0
- agents/task/goals/planner.py +114 -0
- agents/task/logging_config.py +155 -0
- agents/task/path.py +991 -0
- agents/task/robust_parse_config.py +400 -0
- agents/task/runtime/__init__.py +0 -0
- agents/task/runtime/run_as_session.py +130 -0
- agents/task/runtime_safety.py +308 -0
- agents/task/session/__init__.py +5 -0
- agents/task/session/browser_pool.py +130 -0
- agents/task/session/cleanup.py +612 -0
- agents/task/session/execution.py +397 -0
- agents/task/session/feed.py +200 -0
- agents/task/session/hitl_ingress.py +201 -0
- agents/task/session/hooks.py +83 -0
- agents/task/session/multi_agent.py +243 -0
- agents/task/session/workspace.py +91 -0
- agents/task/session_registry.py +91 -0
- agents/task/session_route.py +35 -0
- agents/task/sqlite_session_registry.py +261 -0
- agents/task/surface_config.py +185 -0
- agents/task/telemetry/__init__.py +58 -0
- agents/task/telemetry/event_log.py +180 -0
- agents/task/telemetry/formatters.py +642 -0
- agents/task/telemetry/manager.py +571 -0
- agents/task/telemetry/memory_events.py +66 -0
- agents/task/telemetry/self_events.py +53 -0
- agents/task/telemetry/sequence.py +160 -0
- agents/task/telemetry/service.py +1265 -0
- agents/task/telemetry/views.py +882 -0
- agents/task/templates.py +98 -0
- agents/task/test_aux_chain.py +151 -0
- agents/task/test_aux_router.py +41 -0
- agents/task/tests/test_parse_robustness.py +170 -0
- agents/task/tool_defaults.py +110 -0
- agents/task/utils.py +762 -0
- agents/task/utils_json.py +768 -0
- agents/task/utils_webview.py +86 -0
- agents/task/workspace_context.py +229 -0
- agents/task_agent_lite.py +2398 -0
- api/README.md +683 -0
- api/TASK_API_DOCS.md +151 -0
- api/__init__.py +0 -0
- api/a2a/__init__.py +37 -0
- api/a2a/agent_card.py +337 -0
- api/a2a/client.py +657 -0
- api/a2a/endpoints.py +375 -0
- api/a2a/models.py +268 -0
- api/a2a/streaming.py +419 -0
- api/a2a/task_handler.py +929 -0
- api/admin_endpoints.py +1584 -0
- api/app.py +923 -0
- api/auth_constants.py +28 -0
- api/auth_endpoints.py +402 -0
- api/auth_state.py +36 -0
- api/chat_via_task.py +42 -0
- api/conversation_manager.py +231 -0
- api/dependencies.py +186 -0
- api/eip8004_endpoints.py +452 -0
- api/hyperliquid_models.py +174 -0
- api/hyperliquid_routes.py +450 -0
- api/interfaces.py +207 -0
- api/jwt_middleware.py +83 -0
- api/kb/__init__.py +1 -0
- api/kb/endpoints.py +314 -0
- api/mcp_models.py +279 -0
- api/mcp_routes.py +438 -0
- api/middleware.py +526 -0
- api/models.py +176 -0
- api/openai_compat/__init__.py +0 -0
- api/openai_compat/model_map.py +25 -0
- api/openai_compat/models.py +59 -0
- api/openai_compat/router.py +140 -0
- api/payment_endpoints.py +444 -0
- api/payment_verification.py +138 -0
- api/polymarket_models.py +155 -0
- api/polymarket_routes.py +501 -0
- api/pricing_endpoints.py +89 -0
- api/session_routing.py +54 -0
- api/skill_endpoints.py +453 -0
- api/task_http_api.py +1856 -0
- api/webhooks.py +52 -0
- api/x402_endpoints.py +280 -0
- avatar/README.md +44 -0
- avatar/__init__.py +1 -0
- avatar/config/rob.json +7 -0
- avatar/mindprint.js +395 -0
- avatar/renders/rob.meta.json +20 -0
- avatar/renders/rob.png +0 -0
- avatar/studio.html +373 -0
- avatar/webview/avatar-live.js +44 -0
- cli/README.md +89 -0
- cli/__init__.py +0 -0
- cli/cli_surface.py +67 -0
- cli/commands/__init__.py +0 -0
- cli/commands/_bootstrap.py +49 -0
- cli/commands/_errors.py +53 -0
- cli/commands/chat.py +973 -0
- cli/commands/config.py +100 -0
- cli/commands/dashboard.py +103 -0
- cli/commands/doctor.py +222 -0
- cli/commands/email.py +174 -0
- cli/commands/gateway.py +346 -0
- cli/commands/goals.py +509 -0
- cli/commands/init.py +276 -0
- cli/commands/journey.py +30 -0
- cli/commands/kb.py +280 -0
- cli/commands/model.py +107 -0
- cli/commands/owner.py +431 -0
- cli/commands/pfp.py +232 -0
- cli/commands/run.py +414 -0
- cli/commands/serve.py +41 -0
- cli/commands/session.py +684 -0
- cli/commands/skill_install.py +786 -0
- cli/commands/skills.py +128 -0
- cli/commands/subagents.py +100 -0
- cli/commands/surface.py +81 -0
- cli/commands/telegram.py +185 -0
- cli/commands/todos.py +209 -0
- cli/commands/tools.py +102 -0
- cli/commands/update.py +316 -0
- cli/commands/whatsapp.py +175 -0
- cli/config_store.py +276 -0
- cli/gitignore.py +43 -0
- cli/inventory.py +46 -0
- cli/keys.py +101 -0
- cli/persona.py +17 -0
- cli/polyrob.py +137 -0
- cli/ui/__init__.py +64 -0
- cli/ui/activity.py +141 -0
- cli/ui/app.py +538 -0
- cli/ui/banner.py +233 -0
- cli/ui/blocks.py +534 -0
- cli/ui/bootstrap_notice.py +47 -0
- cli/ui/commands/__init__.py +41 -0
- cli/ui/commands/h_cron.py +64 -0
- cli/ui/commands/h_journey.py +164 -0
- cli/ui/commands/h_kb.py +121 -0
- cli/ui/commands/h_learn.py +100 -0
- cli/ui/commands/h_mcp.py +215 -0
- cli/ui/commands/h_self.py +113 -0
- cli/ui/commands/h_skills.py +314 -0
- cli/ui/commands/handlers.py +1672 -0
- cli/ui/commands/registry.py +426 -0
- cli/ui/dialog.py +425 -0
- cli/ui/event_registry.py +110 -0
- cli/ui/event_specs.py +45 -0
- cli/ui/events.py +433 -0
- cli/ui/identity.py +15 -0
- cli/ui/lifecycle.py +159 -0
- cli/ui/live_hooks.py +43 -0
- cli/ui/model_selector.py +532 -0
- cli/ui/persistent_loop.py +181 -0
- cli/ui/pick.py +113 -0
- cli/ui/plain_renderer.py +351 -0
- cli/ui/renderer.py +384 -0
- cli/ui/rich_renderer.py +533 -0
- cli/ui/secrets.py +115 -0
- cli/ui/state.py +376 -0
- cli/ui/statusbar.py +231 -0
- cli/ui/streaming.py +254 -0
- cli/ui/terminal_render.py +105 -0
- cli/ui/theme.py +132 -0
- cli/update/__init__.py +6 -0
- cli/update/context.py +121 -0
- cli/update/detect.py +176 -0
- cli/update/engine.py +95 -0
- cli/update/migrate_guarded.py +55 -0
- cli/update/process_guard.py +215 -0
- cli/update/runners.py +78 -0
- cli/update/snapshot.py +270 -0
- cli/update/versions.py +205 -0
- core/README.md +377 -0
- core/__init__.py +174 -0
- core/assets.py +87 -0
- core/async_bridge.py +76 -0
- core/autonomy_runtime.py +259 -0
- core/base_component.py +302 -0
- core/bootstrap.py +681 -0
- core/bot.py +310 -0
- core/config.py +833 -0
- core/constants.py +126 -0
- core/container.py +462 -0
- core/db_manifest.py +101 -0
- core/embedding.py +57 -0
- core/env.py +39 -0
- core/exceptions.py +287 -0
- core/flags.py +144 -0
- core/flags_catalog.py +345 -0
- core/home_migration.py +62 -0
- core/identity.py +118 -0
- core/initialization.py +1004 -0
- core/instance.py +537 -0
- core/interactive_gate.py +96 -0
- core/logging.py +436 -0
- core/owner_doc_writer.py +176 -0
- core/pairing.py +193 -0
- core/path_safety.py +20 -0
- core/paths.py +27 -0
- core/payment_config.py +21 -0
- core/permissions.py +322 -0
- core/runtime_config.py +96 -0
- core/runtime_paths.py +135 -0
- core/seams.py +35 -0
- core/secret_scan.py +25 -0
- core/secret_scrub.py +71 -0
- core/secrets.py +35 -0
- core/security_logging_filter.py +148 -0
- core/self_context_writer.py +333 -0
- core/self_evolution.py +294 -0
- core/session_context.py +47 -0
- core/sqlite_util.py +59 -0
- core/surfaces/__init__.py +16 -0
- core/surfaces/access.py +118 -0
- core/surfaces/binding.py +88 -0
- core/surfaces/bootstrap.py +78 -0
- core/surfaces/circuit.py +168 -0
- core/surfaces/continuity.py +77 -0
- core/surfaces/correspondents.py +228 -0
- core/surfaces/dispatcher.py +216 -0
- core/surfaces/envelopes.py +75 -0
- core/surfaces/gc.py +30 -0
- core/surfaces/idempotency.py +57 -0
- core/surfaces/inbound_webhook.py +124 -0
- core/surfaces/media.py +32 -0
- core/surfaces/message_router.py +91 -0
- core/surfaces/outbound_allowlist.py +80 -0
- core/surfaces/outbound_dispatcher.py +110 -0
- core/surfaces/outbound_mirror.py +37 -0
- core/surfaces/outbound_queue.py +107 -0
- core/surfaces/outbound_target.py +12 -0
- core/surfaces/owner_admin.py +29 -0
- core/surfaces/proactive.py +78 -0
- core/surfaces/progress.py +108 -0
- core/surfaces/rate_bucket.py +21 -0
- core/surfaces/registry.py +51 -0
- core/surfaces/rendering.py +53 -0
- core/surfaces/send_policy.py +14 -0
- core/surfaces/serialize.py +24 -0
- core/surfaces/session_chat_registry.py +112 -0
- core/surfaces/session_policy.py +60 -0
- core/surfaces/surface.py +288 -0
- core/surfaces/transcription.py +88 -0
- core/surfaces/voice_echo.py +26 -0
- core/surfaces/voice_guard.py +23 -0
- core/tickers.py +174 -0
- core/tool_catalog.py +192 -0
- core/version.py +75 -0
- core/wallet/__init__.py +5 -0
- core/wallet/agent_wallet.py +59 -0
- core/wallet/audit_sink.py +61 -0
- core/wallet/config.py +70 -0
- core/wallet/factory.py +87 -0
- core/wallet/policy.py +104 -0
- core/wallet/signer.py +51 -0
- cron/README.md +65 -0
- cron/__init__.py +0 -0
- cron/delivery.py +277 -0
- cron/digest.py +124 -0
- cron/jobs.py +207 -0
- cron/runner.py +297 -0
- cron/schedule.py +194 -0
- cron/scheduler.py +201 -0
- cron/service.py +57 -0
- cron/wake_gate.py +223 -0
- data/__init__.py +1 -0
- data/prompts/autov2_prompts.json +87 -0
- data/prompts/skills/browser-automation/SKILL.md +43 -0
- data/prompts/skills/coding-workflow/SKILL.md +36 -0
- data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
- data/prompts/skills/document-writing/SKILL.md +33 -0
- data/prompts/skills/email-comms/SKILL.md +33 -0
- data/prompts/skills/file-data-ops/SKILL.md +35 -0
- data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
- data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
- data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
- data/prompts/skills/lead-research/SKILL.md +86 -0
- data/prompts/skills/market-research-brief/SKILL.md +104 -0
- data/prompts/skills/person-analyzer/SKILL.md +124 -0
- data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
- data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
- data/prompts/skills/polymarket-trading/SKILL.md +55 -0
- data/prompts/skills/presentation-creator/SKILL.md +36 -0
- data/prompts/skills/project-analyzer/SKILL.md +169 -0
- data/prompts/skills/rules.json +268 -0
- data/prompts/skills/secret-handling/SKILL.md +20 -0
- data/prompts/skills/skill-authoring/SKILL.md +31 -0
- data/prompts/skills/skill-security-review/SKILL.md +27 -0
- data/prompts/skills/social-discovery/SKILL.md +57 -0
- data/prompts/skills/task-planning/SKILL.md +33 -0
- data/prompts/skills/web-research/SKILL.md +39 -0
- data/prompts/skills/web-scraping/SKILL.md +73 -0
- data/prompts/skills/x-engagement/SKILL.md +69 -0
- data/prompts/system_prompts.json +4 -0
- data/subscription_channels.json +5 -0
- modules/README.md +503 -0
- modules/__init__.py +124 -0
- modules/auth/__init__.py +13 -0
- modules/auth/api_key_manager.py +157 -0
- modules/auth/identity_mapper.py +380 -0
- modules/auth/siwe_auth.py +273 -0
- modules/auth/tier_manager.py +173 -0
- modules/base_module.py +190 -0
- modules/credits/__init__.py +29 -0
- modules/credits/balance_manager.py +237 -0
- modules/credits/cost_utils.py +129 -0
- modules/credits/pricing.py +139 -0
- modules/credits/unified_ledger.py +148 -0
- modules/credits/usage_meter.py +310 -0
- modules/credits/usage_tracker.py +674 -0
- modules/database/__init__.py +33 -0
- modules/database/audit_log.py +322 -0
- modules/database/auth_tables.py +276 -0
- modules/database/connection.py +644 -0
- modules/database/connection_pool.py +305 -0
- modules/database/conversation_contexts.py +446 -0
- modules/database/database_manager.py +237 -0
- modules/database/hyperliquid.py +375 -0
- modules/database/polymarket.py +574 -0
- modules/database/user_mcp_servers.py +728 -0
- modules/database/user_profiles.py +562 -0
- modules/database/utils.py +107 -0
- modules/database/x402_tables.py +92 -0
- modules/eip8004/README.md +1069 -0
- modules/eip8004/__init__.py +65 -0
- modules/eip8004/contracts.py +498 -0
- modules/eip8004/models.py +271 -0
- modules/eip8004/registration.py +148 -0
- modules/eip8004/reputation.py +367 -0
- modules/eip8004/validation.py +277 -0
- modules/llm/__init__.py +97 -0
- modules/llm/adapters.py +963 -0
- modules/llm/anthropic_client.py +831 -0
- modules/llm/available_models.py +106 -0
- modules/llm/brain_scrubber.py +137 -0
- modules/llm/cache_hints.py +162 -0
- modules/llm/deepseek_client.py +562 -0
- modules/llm/gemini_client.py +1367 -0
- modules/llm/llm_client.py +1139 -0
- modules/llm/llm_client_registry.py +188 -0
- modules/llm/llm_factory.py +237 -0
- modules/llm/llm_manager.py +1036 -0
- modules/llm/messages.py +374 -0
- modules/llm/model_registry.py +1935 -0
- modules/llm/nvidia_client.py +125 -0
- modules/llm/openai_client.py +800 -0
- modules/llm/openrouter_client.py +969 -0
- modules/llm/profiles.py +178 -0
- modules/llm/test_anthropic_cache_capture.py +35 -0
- modules/llm/test_cached_pricing_derivation.py +24 -0
- modules/llm/think_scrubber.py +444 -0
- modules/llm/token_counter.py +268 -0
- modules/memory/__init__.py +35 -0
- modules/memory/backend_factory.py +83 -0
- modules/memory/cache_manager.py +186 -0
- modules/memory/episodic.py +108 -0
- modules/memory/local_vector_memory_provider.py +552 -0
- modules/memory/memory_manager.py +225 -0
- modules/memory/models.py +312 -0
- modules/memory/provider.py +210 -0
- modules/memory/registry.py +205 -0
- modules/memory/sqlite_memory_provider.py +767 -0
- modules/memory/task/__init__.py +52 -0
- modules/memory/task/compaction_manager.py +157 -0
- modules/memory/task/context_retriever.py +809 -0
- modules/memory/task/hierarchical_memory.py +1002 -0
- modules/memory/task/lexical_retriever.py +71 -0
- modules/memory/task/null_context_manager.py +213 -0
- modules/memory/task/phase_manager.py +625 -0
- modules/memory/task/reflection_service.py +157 -0
- modules/memory/task/semantic_retriever.py +218 -0
- modules/memory/task/task_context_manager.py +1165 -0
- modules/memory/task/test_forgetting_engages.py +24 -0
- modules/memory/task/test_reflection_llm.py +39 -0
- modules/memory/task/test_threat_scan.py +53 -0
- modules/memory/task/threat_scan.py +90 -0
- modules/memory/test_sqlite_memory_provider.py +106 -0
- modules/memory/user_profile_manager.py +527 -0
- modules/payments/__init__.py +11 -0
- modules/payments/deposit_monitor.py +478 -0
- modules/payments/price_oracle.py +70 -0
- modules/payments/treasury_sweeper.py +401 -0
- modules/payments/wallet_generator.py +145 -0
- modules/pfp/__init__.py +1 -0
- modules/pfp/config.py +92 -0
- modules/pfp/mesh.py +565 -0
- modules/pfp/push.py +69 -0
- modules/pfp/renderer.py +85 -0
- modules/pfp/store.py +82 -0
- modules/skills/__init__.py +1 -0
- modules/skills/skill_usage.py +248 -0
- modules/transcription/__init__.py +24 -0
- modules/transcription/base.py +20 -0
- modules/transcription/faster_whisper_transcriber.py +55 -0
- modules/x402/README.md +249 -0
- modules/x402/__init__.py +22 -0
- modules/x402/invoicing.py +416 -0
- modules/x402/middleware.py +364 -0
- modules/x402/settlement_watcher.py +145 -0
- modules/x402/x402_integration.py +344 -0
- polyrob-0.5.0.dist-info/METADATA +482 -0
- polyrob-0.5.0.dist-info/RECORD +745 -0
- polyrob-0.5.0.dist-info/WHEEL +5 -0
- polyrob-0.5.0.dist-info/entry_points.txt +2 -0
- polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
- polyrob-0.5.0.dist-info/top_level.txt +12 -0
- surfaces/README.md +70 -0
- surfaces/__init__.py +0 -0
- surfaces/email/__init__.py +0 -0
- surfaces/email/dedup.py +44 -0
- surfaces/email/harness.py +187 -0
- surfaces/email/inbound.py +159 -0
- surfaces/email/seed.py +60 -0
- surfaces/email/surface.py +105 -0
- surfaces/telegram/__init__.py +0 -0
- surfaces/telegram/dedup.py +83 -0
- surfaces/telegram/harness.py +886 -0
- surfaces/telegram/inbound.py +145 -0
- surfaces/telegram/interactive_tools.py +49 -0
- surfaces/telegram/markdown.py +347 -0
- surfaces/telegram/rate_limit.py +167 -0
- surfaces/telegram/surface.py +187 -0
- surfaces/telegram/voice.py +52 -0
- surfaces/whatsapp/__init__.py +0 -0
- surfaces/whatsapp/client.py +51 -0
- surfaces/whatsapp/harness.py +62 -0
- surfaces/whatsapp/inbound.py +132 -0
- surfaces/whatsapp/surface.py +64 -0
- surfaces/whatsapp/window.py +34 -0
- tools/README.md +578 -0
- tools/__init__.py +424 -0
- tools/alchemy/__init__.py +5 -0
- tools/alchemy/alchemy_tool.py +389 -0
- tools/anysite/__init__.py +17 -0
- tools/anysite/client.py +88 -0
- tools/anysite/tool.py +89 -0
- tools/base_tool.py +468 -0
- tools/browser/__init__.py +22 -0
- tools/browser/actions.py +118 -0
- tools/browser/browser.py +1457 -0
- tools/browser/browser_manager.py +690 -0
- tools/browser/context.py +1748 -0
- tools/browser/playwright_utils.py +137 -0
- tools/browser/views.py +56 -0
- tools/code_exec/SANDBOX_SECURITY.md +144 -0
- tools/code_exec/__init__.py +178 -0
- tools/code_exec/backend.py +73 -0
- tools/code_exec/backends/__init__.py +1 -0
- tools/code_exec/backends/docker.py +779 -0
- tools/code_exec/backends/local_subprocess.py +154 -0
- tools/code_exec/env_policy.py +45 -0
- tools/code_exec/result.py +42 -0
- tools/code_exec/sandbox_guard.py +59 -0
- tools/code_exec/tool.py +227 -0
- tools/coding/__init__.py +50 -0
- tools/coding/edit.py +107 -0
- tools/coding/search.py +113 -0
- tools/coding/tool.py +353 -0
- tools/collabland/__init__.py +5 -0
- tools/collabland/collabland_tool.py +471 -0
- tools/controller/__init__.py +0 -0
- tools/controller/_helpers.py +165 -0
- tools/controller/action_registration.py +1765 -0
- tools/controller/approval.py +244 -0
- tools/controller/approval_interactive.py +90 -0
- tools/controller/delegation.py +195 -0
- tools/controller/execution.py +797 -0
- tools/controller/execution_context.py +141 -0
- tools/controller/hooks.py +135 -0
- tools/controller/introspection.py +344 -0
- tools/controller/mcp_registrar.py +190 -0
- tools/controller/message_send.py +31 -0
- tools/controller/registry/__init__.py +4 -0
- tools/controller/registry/schema_generators.py +395 -0
- tools/controller/registry/schema_sanitizer.py +406 -0
- tools/controller/registry/service.py +1266 -0
- tools/controller/registry/views.py +151 -0
- tools/controller/service.py +409 -0
- tools/controller/tool_management.py +334 -0
- tools/controller/types.py +28 -0
- tools/controller/views.py +450 -0
- tools/cronjob_tools.py +166 -0
- tools/crypto_trade_gate.py +59 -0
- tools/descriptors.py +452 -0
- tools/dom/__init__.py +0 -0
- tools/dom/history_tree_processor/service.py +244 -0
- tools/dom/history_tree_processor/view.py +99 -0
- tools/dom/service.py +471 -0
- tools/dom/views.py +346 -0
- tools/email_tool.py +403 -0
- tools/exceptions.py +169 -0
- tools/filesystem.py +1038 -0
- tools/filesystem_docproc.py +331 -0
- tools/filesystem_pdf.py +755 -0
- tools/git/__init__.py +42 -0
- tools/git/tool.py +258 -0
- tools/github/__init__.py +33 -0
- tools/github/client.py +94 -0
- tools/github/tool.py +237 -0
- tools/goal_tools.py +309 -0
- tools/hyperliquid/__init__.py +56 -0
- tools/hyperliquid/models.py +233 -0
- tools/hyperliquid/service.py +1680 -0
- tools/knowledge_ingest.py +950 -0
- tools/mcp/README.md +535 -0
- tools/mcp/__init__.py +52 -0
- tools/mcp/catalog.py +115 -0
- tools/mcp/config.py +337 -0
- tools/mcp/mcp_tool.py +1415 -0
- tools/mcp/param_coercion.py +289 -0
- tools/mcp/protocol.py +1520 -0
- tools/mcp/rate_limit.py +50 -0
- tools/mcp/security.py +424 -0
- tools/mcp/self_install.py +111 -0
- tools/mcp/server_manager.py +829 -0
- tools/mcp/subscriptions.py +72 -0
- tools/mcp/user_mcp_service.py +899 -0
- tools/mcp/validation_tracker.py +89 -0
- tools/mcp/views.py +346 -0
- tools/oauth/__init__.py +17 -0
- tools/oauth/manager.py +75 -0
- tools/oauth/provider.py +69 -0
- tools/oauth/providers/__init__.py +1 -0
- tools/oauth/providers/generic_oauth2.py +96 -0
- tools/perplexity_tool.py +280 -0
- tools/polymarket/__init__.py +41 -0
- tools/polymarket/clob_adapter.py +76 -0
- tools/polymarket/models.py +182 -0
- tools/polymarket/service.py +2082 -0
- tools/self_env/__init__.py +61 -0
- tools/self_env/tool.py +274 -0
- tools/shell/__init__.py +80 -0
- tools/shell/backend_pool.py +118 -0
- tools/shell/discipline.py +62 -0
- tools/shell/executor.py +157 -0
- tools/shell/loopback_allow.py +87 -0
- tools/shell/process_registry.py +113 -0
- tools/shell/process_tool.py +131 -0
- tools/shell/state.py +129 -0
- tools/shell/tool.py +151 -0
- tools/task_tool.py +637 -0
- tools/twitter_tool.py +2086 -0
- tools/user_directory.py +179 -0
- tools/web_fetch/__init__.py +3 -0
- tools/web_fetch/fetcher.py +144 -0
- tools/web_fetch/render.py +44 -0
- tools/web_fetch/tool.py +60 -0
- tools/x402/__init__.py +60 -0
- tools/x402/client.py +47 -0
- tools/x402/invoice_tool.py +143 -0
- tools/x402/real_client.py +250 -0
- tools/x402/service.py +131 -0
- utils/README.md +498 -0
- utils/__init__.py +46 -0
- utils/auth_utils.py +114 -0
- utils/bounded_collections.py +162 -0
- utils/circuit_breaker.py +389 -0
- utils/gif_utils.py +435 -0
- utils/markdown_utils.py +291 -0
- utils/message_utils.py +147 -0
- utils/metrics.py +36 -0
- utils/path_validator.py +227 -0
- utils/rate_limit_manager.py +399 -0
- utils/result_size.py +296 -0
- utils/time_utils.py +206 -0
- utils/user_utils.py +188 -0
- webview/README.md +111 -0
- webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
- webview/__init__.py +8 -0
- webview/activity.py +636 -0
- webview/owner_auth.py +108 -0
- webview/pages.py +448 -0
- webview/repair_sessions.py +399 -0
- webview/server.py +4323 -0
- webview/server_launcher.py +112 -0
- webview/static/css/activity.css +89 -0
- webview/static/css/chat.css +1772 -0
- webview/static/css/components.css +1190 -0
- webview/static/css/config-panel.css +651 -0
- webview/static/css/pages/admin.css +766 -0
- webview/static/css/pages/profile.css +231 -0
- webview/static/css/pages/settings.css +297 -0
- webview/static/css/pages/signin.css +316 -0
- webview/static/css/style.css +4250 -0
- webview/static/css/variables.css +147 -0
- webview/static/css/workspace-fullwidth-fix.css +109 -0
- webview/static/img/favicon.ico +0 -0
- webview/static/js/activity.js +394 -0
- webview/static/js/admin/activity.js +186 -0
- webview/static/js/admin/dashboard.js +163 -0
- webview/static/js/admin/user_detail.js +400 -0
- webview/static/js/admin/users.js +174 -0
- webview/static/js/admin/utils.js +214 -0
- webview/static/js/chat.js +3178 -0
- webview/static/js/config-panel.js +522 -0
- webview/static/js/constants.js +20 -0
- webview/static/js/error-handler.js +226 -0
- webview/static/js/ethers.min.js +1 -0
- webview/static/js/event-filter.js +137 -0
- webview/static/js/event-store.js +351 -0
- webview/static/js/file-attachments.js +374 -0
- webview/static/js/file-loader.js +163 -0
- webview/static/js/index.js +103 -0
- webview/static/js/performance-utils.js +196 -0
- webview/static/js/profile.js +396 -0
- webview/static/js/screenshot.js +386 -0
- webview/static/js/session.js +1527 -0
- webview/static/js/settings.js +1288 -0
- webview/static/js/sidebar-data.js +766 -0
- webview/static/js/sidebar-toggle.js +296 -0
- webview/static/js/socket.io.min.js +7 -0
- webview/static/js/stats.js +355 -0
- webview/static/js/ui-utils.js +420 -0
- webview/static/js/workspace.js +966 -0
- webview/stats_service.py +613 -0
- webview/templates/__init__.py +1 -0
- webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- webview/templates/activity.html +36 -0
- webview/templates/admin/activity.html +122 -0
- webview/templates/admin/dashboard.html +112 -0
- webview/templates/admin/user_detail.html +228 -0
- webview/templates/admin/users.html +105 -0
- webview/templates/autonomy.html +60 -0
- webview/templates/error.html +133 -0
- webview/templates/finance.html +90 -0
- webview/templates/identity.html +73 -0
- webview/templates/index.html +42 -0
- webview/templates/layout.html +125 -0
- webview/templates/memory.html +59 -0
- webview/templates/owner_login.html +15 -0
- webview/templates/profile.html +121 -0
- webview/templates/session.html +312 -0
- webview/templates/settings.html +534 -0
- webview/templates/sidebar.html +112 -0
- webview/templates/signin.html +213 -0
- webview/templates/status.html +14 -0
- webview/templates/system.html +45 -0
- webview/webgate.py +196 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
"""Skill storage scopes (Task 8 — data-safety fix).
|
|
2
|
+
|
|
3
|
+
Splits "where do skills live" into named, precedence-ordered SCOPES so writable
|
|
4
|
+
per-tenant skills can move OUT of the installed package/code tree and into
|
|
5
|
+
data-home, where they survive a ``polyrob update`` code-swap.
|
|
6
|
+
|
|
7
|
+
Before this module existed, ``SkillManager``/``SkillWriterMixin`` wrote
|
|
8
|
+
``user_<uid>/`` skills directly under the same directory that ships the
|
|
9
|
+
curated SYSTEM skills (``data/prompts/skills/``) — a directory that lives
|
|
10
|
+
INSIDE the installed code/package tree. ``polyrob update``'s code-swap
|
|
11
|
+
replaces that whole tree, so every self-authored skill a user/agent had built
|
|
12
|
+
up was silently destroyed on update.
|
|
13
|
+
|
|
14
|
+
Three named scopes, precedence **project > user > builtin**:
|
|
15
|
+
|
|
16
|
+
- **project** — a per-repo ``.agents/skills`` / ``.claude/skills`` dir.
|
|
17
|
+
STUB: Task 15 owns real discovery/loading; this module only reserves the
|
|
18
|
+
scope + precedence slot so that later work doesn't have to re-litigate
|
|
19
|
+
ordering. Not read from or written to yet.
|
|
20
|
+
- **user** — ``<data_home>/skills/user_<uid>/``. Writable; survives a
|
|
21
|
+
code-tree swap because data-home is never touched by ``polyrob update``.
|
|
22
|
+
- **builtin** — the shipped package ``data/prompts/skills/`` (the curated
|
|
23
|
+
system-skill library). Read-only, pre-vetted/trusted.
|
|
24
|
+
|
|
25
|
+
``<data_home>`` is resolved via the EXACT same accessor
|
|
26
|
+
``cli/update/context.py`` uses for its own data-home
|
|
27
|
+
(``core/runtime_paths.py::resolve_runtime_paths``), so ``POLYROB_DATA_DIR`` is
|
|
28
|
+
honored identically across the update and skill-storage code paths.
|
|
29
|
+
|
|
30
|
+
NOTE on migration: this module only changes WHERE NEW reads/writes land. Any
|
|
31
|
+
``user_<uid>/`` directories that predate this change and still live under the
|
|
32
|
+
package tree are NOT moved by this module — that one-time migration is a
|
|
33
|
+
separate, deliberately-scoped follow-up (Task 9) so a data-moving operation
|
|
34
|
+
gets its own dedicated review.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
import errno
|
|
38
|
+
import hashlib
|
|
39
|
+
import json
|
|
40
|
+
import logging
|
|
41
|
+
import os
|
|
42
|
+
import shutil
|
|
43
|
+
import tempfile
|
|
44
|
+
from dataclasses import dataclass
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from typing import List, Optional
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class SkillScope:
|
|
53
|
+
"""One named root that skills can be read from / written to.
|
|
54
|
+
|
|
55
|
+
``trusted`` describes the LOCATION's provenance (pre-vetted shipped
|
|
56
|
+
content vs. arbitrary/unreviewed content) — it is NOT a blanket "skip
|
|
57
|
+
security scanning" flag. See :func:`scan_exempt`, which additionally
|
|
58
|
+
requires ``not writable`` so a writable scope (today: user) can never
|
|
59
|
+
have its write-time content scan suppressed just because the scope is
|
|
60
|
+
marked trusted.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
name: str
|
|
64
|
+
root: Path
|
|
65
|
+
writable: bool
|
|
66
|
+
trusted: bool
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# The literal on-disk directory name under a scope's data-home root.
|
|
70
|
+
_SKILLS_SUBDIR = "skills"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _builtin_root() -> Path:
|
|
74
|
+
"""The installed package's shipped (builtin) skills directory.
|
|
75
|
+
|
|
76
|
+
Resolved from THIS file's own location rather than importing
|
|
77
|
+
``skill_manager`` — the brief's suggested
|
|
78
|
+
``Path(skill_manager.__file__).resolve().parents[3]`` would create a
|
|
79
|
+
``skill_store`` <-> ``skill_manager`` import cycle, since ``skill_manager``
|
|
80
|
+
needs ``skill_store`` for the user/data-home scope. ``skill_store.py``
|
|
81
|
+
lives in the SAME directory as ``skill_manager.py``
|
|
82
|
+
(``agents/task/agent/``), so the identical ``parents[3]`` hop from this
|
|
83
|
+
file resolves to the same repo/package root:
|
|
84
|
+
``agents/task/agent/<this file>`` -> parents[0]=agent, [1]=task,
|
|
85
|
+
[2]=agents, [3]=repo root. Verified empirically: this path exists and
|
|
86
|
+
contains ``rules.json`` (see ``test_skill_store_scopes.py``).
|
|
87
|
+
"""
|
|
88
|
+
return Path(__file__).resolve().parents[3] / "data" / "prompts" / "skills"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _data_home() -> Path:
|
|
92
|
+
"""Resolve ``<data_home>`` the SAME way ``cli/update/context.py`` does,
|
|
93
|
+
so ``POLYROB_DATA_DIR`` (and the local-vs-server default split) is
|
|
94
|
+
honored identically for skill storage and for update snapshot/rollback.
|
|
95
|
+
"""
|
|
96
|
+
from core.runtime_paths import resolve_runtime_paths
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
from agents.task.constants import local_mode_enabled
|
|
100
|
+
|
|
101
|
+
local = local_mode_enabled()
|
|
102
|
+
except Exception:
|
|
103
|
+
local = False
|
|
104
|
+
rp = resolve_runtime_paths(local=local)
|
|
105
|
+
return Path(rp.data_home)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def skills_data_home() -> Path:
|
|
109
|
+
"""``<data_home>/skills`` — the writable root for user-authored skills."""
|
|
110
|
+
return _data_home() / _SKILLS_SUBDIR
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def builtin_scope() -> SkillScope:
|
|
114
|
+
"""The shipped, read-only, pre-vetted system-skill library.
|
|
115
|
+
|
|
116
|
+
NOTE: this scope's root MAY also physically hold legacy ``user_<uid>/``
|
|
117
|
+
dirs at runtime until Task 9's migration moves them into ``user_scope()``.
|
|
118
|
+
Callers enumerating "the builtin skills" MUST exclude ``user_*``/dotted
|
|
119
|
+
entries (see :func:`builtin_skill_ids`) rather than assuming every child
|
|
120
|
+
of this root is a system skill.
|
|
121
|
+
"""
|
|
122
|
+
return SkillScope(name="builtin", root=_builtin_root(), writable=False, trusted=True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def user_scope() -> SkillScope:
|
|
126
|
+
"""The writable, tenant-scoped, data-home skill root (survives updates)."""
|
|
127
|
+
return SkillScope(name="user", root=skills_data_home(), writable=True, trusted=True)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _find_project_root_stub() -> Path:
|
|
131
|
+
"""STUB project-skills root (Task 15 owns real discovery/loading).
|
|
132
|
+
|
|
133
|
+
Walks CWD up to the git root looking for a conventional per-repo skills
|
|
134
|
+
dir (``.agents/skills`` then ``.claude/skills``); falls back to
|
|
135
|
+
``<cwd>/.agents/skills`` (need not exist) so ``project_scope()`` always
|
|
136
|
+
has a concrete ``root``. Bounded walk — never infinite even with an
|
|
137
|
+
unusual filesystem layout.
|
|
138
|
+
"""
|
|
139
|
+
cur = Path.cwd().resolve()
|
|
140
|
+
for _ in range(64):
|
|
141
|
+
for rel in (".agents/skills", ".claude/skills"):
|
|
142
|
+
cand = cur / rel
|
|
143
|
+
if cand.is_dir():
|
|
144
|
+
return cand
|
|
145
|
+
if (cur / ".git").exists():
|
|
146
|
+
break
|
|
147
|
+
parent = cur.parent
|
|
148
|
+
if parent == cur:
|
|
149
|
+
break
|
|
150
|
+
cur = parent
|
|
151
|
+
return Path.cwd().resolve() / ".agents" / "skills"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def project_scope() -> SkillScope:
|
|
155
|
+
"""Per-repo skills scope. `trusted` reflects the runtime trust gate so a caller that
|
|
156
|
+
inspects the scope sees the real policy; discovery still gates on
|
|
157
|
+
skill_discovery.trust_project_skills_effective().
|
|
158
|
+
"""
|
|
159
|
+
from . import skill_discovery
|
|
160
|
+
trusted = False
|
|
161
|
+
try:
|
|
162
|
+
trusted = skill_discovery.trust_project_skills_effective()
|
|
163
|
+
except Exception:
|
|
164
|
+
trusted = False
|
|
165
|
+
root = _find_project_root_stub()
|
|
166
|
+
return SkillScope(name="project", root=root, writable=False, trusted=trusted)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def resolve_scopes() -> List[SkillScope]:
|
|
170
|
+
"""All skill scopes in precedence order: project > user > builtin."""
|
|
171
|
+
return [project_scope(), user_scope(), builtin_scope()]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def scan_exempt(scope: SkillScope) -> bool:
|
|
175
|
+
"""Whether content already resident in ``scope`` can skip a write-time
|
|
176
|
+
threat re-scan (future-proofs e.g. a reindex of the shipped library).
|
|
177
|
+
|
|
178
|
+
Only ``True`` for a read-only + trusted scope (today: builtin). A
|
|
179
|
+
writable scope is NEVER exempt regardless of its ``trusted`` flag —
|
|
180
|
+
``trusted`` describes a root's provenance, not a license to skip scanning
|
|
181
|
+
NEW content written into it.
|
|
182
|
+
|
|
183
|
+
``SkillWriterMixin.create_skill`` only ever targets ``user_scope()``
|
|
184
|
+
(``writable=True``), so this evaluates to ``False`` on every live write
|
|
185
|
+
call today — a deliberate no-op that keeps user-write scanning
|
|
186
|
+
unconditional. It exists so a future builtin-library reindex can reuse
|
|
187
|
+
one policy function instead of re-deriving it.
|
|
188
|
+
"""
|
|
189
|
+
return scope.trusted and not scope.writable
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def builtin_skill_ids(scope: Optional[SkillScope] = None) -> List[str]:
|
|
193
|
+
"""Sorted ids of the shipped system skills under ``scope`` (default:
|
|
194
|
+
:func:`builtin_scope`), EXCLUDING ``user_*`` and dotted directories.
|
|
195
|
+
|
|
196
|
+
Mirrors ``SkillManager._iter_authored_skill_dirs``'s filter so "what
|
|
197
|
+
counts as a builtin skill" can't drift between the two call sites.
|
|
198
|
+
"""
|
|
199
|
+
scope = scope or builtin_scope()
|
|
200
|
+
root = scope.root
|
|
201
|
+
if not root.is_dir():
|
|
202
|
+
return []
|
|
203
|
+
return sorted(
|
|
204
|
+
d.name
|
|
205
|
+
for d in root.iterdir()
|
|
206
|
+
if d.is_dir() and not d.name.startswith((".", "user_")) and (d / "SKILL.md").exists()
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# =============================================================================
|
|
211
|
+
# Task 9: one-time legacy code-tree -> data-home migration
|
|
212
|
+
#
|
|
213
|
+
# Before Task 8, per-tenant ``user_<uid>/`` skills were written directly under
|
|
214
|
+
# the SAME shipped package tree that holds the curated system-skill library
|
|
215
|
+
# (``builtin_scope().root`` — now this module's ``legacy_root`` parameter).
|
|
216
|
+
# Task 8 redirected ``SkillManager``'s user-scope reads/writes to data-home
|
|
217
|
+
# instead; that alone makes any PRE-EXISTING ``user_<uid>/`` content on that
|
|
218
|
+
# tree instantly invisible (no ``polyrob update`` needed - simply running the
|
|
219
|
+
# new code stops looking there), and it would additionally be destroyed
|
|
220
|
+
# outright by the next ``polyrob update`` code-swap. This section moves that
|
|
221
|
+
# legacy content into data-home, once, so a deployment upgrading past Task 8
|
|
222
|
+
# doesn't lose it.
|
|
223
|
+
# =============================================================================
|
|
224
|
+
|
|
225
|
+
_MIGRATION_MARKER_NAME = ".migrated_v1"
|
|
226
|
+
_MIGRATION_LOCK_NAME = ".migrate.lock"
|
|
227
|
+
_SOURCE_RETAINED_BREADCRUMB = ".migrated_source_retained"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _is_safe_component(name: str) -> bool:
|
|
231
|
+
"""Whether a single path-segment name is safe to treat as a migration
|
|
232
|
+
source/destination component: no traversal (``.``/``..``), no embedded
|
|
233
|
+
separators, not empty.
|
|
234
|
+
|
|
235
|
+
Mirrors the SPIRIT of ``SkillManager.validate_skill_id``'s traversal
|
|
236
|
+
protection without importing ``skill_manager`` (would create an import
|
|
237
|
+
cycle - see the ``_builtin_root`` docstring above). Deliberately looser
|
|
238
|
+
than the full id-format regex (which requires lowercase-start/hyphens
|
|
239
|
+
only) so a legitimately-named pre-Task-8 skill isn't silently abandoned
|
|
240
|
+
by migration merely for predating stricter id validation; the traversal
|
|
241
|
+
guard here is a SECURITY check, not a format check.
|
|
242
|
+
"""
|
|
243
|
+
if not name or name in (".", ".."):
|
|
244
|
+
return False
|
|
245
|
+
if "/" in name or "\\" in name or "\x00" in name:
|
|
246
|
+
return False
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _sha256_file(path: Path) -> str:
|
|
251
|
+
h = hashlib.sha256()
|
|
252
|
+
with open(path, "rb") as f:
|
|
253
|
+
for chunk in iter(lambda: f.read(1 << 16), b""):
|
|
254
|
+
h.update(chunk)
|
|
255
|
+
return h.hexdigest()
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _migrate_one_skill(source_dir: Path, dest_dir: Path) -> bool:
|
|
259
|
+
"""Move one skill directory from the legacy code tree into data-home.
|
|
260
|
+
|
|
261
|
+
Never destructive: copy to a temp dir alongside ``dest_dir`` -> sha256
|
|
262
|
+
verify the copied ``SKILL.md`` against the source -> place at ``dest_dir``
|
|
263
|
+
-> ONLY THEN attempt to remove the source. Placement is a fast
|
|
264
|
+
``os.replace`` when same-filesystem; a CROSS-DEVICE placement (``EXDEV``:
|
|
265
|
+
package tree and data-home on different volumes, or a Windows cross-drive
|
|
266
|
+
move) falls back to a plain ``copytree`` that RE-VERIFIES the placed
|
|
267
|
+
destination and then deliberately LEAVES the source in place (copy+verify,
|
|
268
|
+
NO cross-volume remove — the brief's rule, and matching the EACCES stance).
|
|
269
|
+
On the same-filesystem path, the source removal is best-effort: a
|
|
270
|
+
``PermissionError``/``OSError`` (read-only / root-owned site-packages
|
|
271
|
+
install) is tolerated, the source is left with a breadcrumb at the
|
|
272
|
+
destination, never raised, never retried.
|
|
273
|
+
|
|
274
|
+
Returns True iff a NEW, verified copy was placed at ``dest_dir``
|
|
275
|
+
(regardless of whether the legacy source could/should also be cleaned up).
|
|
276
|
+
"""
|
|
277
|
+
if dest_dir.exists():
|
|
278
|
+
logger.debug("skill migration: destination already exists, skipping %s", dest_dir)
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
source_skill_md = source_dir / "SKILL.md"
|
|
282
|
+
if source_skill_md.is_symlink() or not source_skill_md.is_file():
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
source_hash = _sha256_file(source_skill_md)
|
|
287
|
+
except Exception:
|
|
288
|
+
logger.warning("skill migration: could not hash %s - skipped", source_skill_md)
|
|
289
|
+
return False
|
|
290
|
+
|
|
291
|
+
dest_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
tmp_root = Path(tempfile.mkdtemp(prefix=".migrate_tmp_", dir=str(dest_dir.parent)))
|
|
293
|
+
cross_volume = False
|
|
294
|
+
try:
|
|
295
|
+
tmp_skill_dir = tmp_root / dest_dir.name
|
|
296
|
+
shutil.copytree(source_dir, tmp_skill_dir)
|
|
297
|
+
|
|
298
|
+
tmp_skill_md = tmp_skill_dir / "SKILL.md"
|
|
299
|
+
if not tmp_skill_md.is_file() or _sha256_file(tmp_skill_md) != source_hash:
|
|
300
|
+
logger.warning("skill migration: copy verification failed for %s - skipped", source_dir)
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
if dest_dir.exists():
|
|
304
|
+
# Lost a race to a concurrent writer (e.g. a fresh agent-authored
|
|
305
|
+
# skill at the same id) between the earlier check and now - never
|
|
306
|
+
# clobber whatever just landed there.
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
os.replace(str(tmp_skill_dir), str(dest_dir))
|
|
311
|
+
except OSError as e:
|
|
312
|
+
if getattr(e, "errno", None) != errno.EXDEV:
|
|
313
|
+
raise
|
|
314
|
+
# Cross-device: fall back to a plain copy INTO the destination.
|
|
315
|
+
if dest_dir.exists():
|
|
316
|
+
return False
|
|
317
|
+
shutil.copytree(tmp_skill_dir, dest_dir)
|
|
318
|
+
cross_volume = True
|
|
319
|
+
# Re-verify the PLACED destination (not the tmp): a cross-volume
|
|
320
|
+
# copy is exactly the case where a partial/corrupt write is most
|
|
321
|
+
# likely, and because we are about to DELIBERATELY leave the source
|
|
322
|
+
# (no cross-volume remove), the destination must be provably
|
|
323
|
+
# correct. On mismatch, roll back the bad destination and bail so
|
|
324
|
+
# nothing is counted and the source stays untouched.
|
|
325
|
+
dest_skill_md = dest_dir / "SKILL.md"
|
|
326
|
+
try:
|
|
327
|
+
dest_ok = dest_skill_md.is_file() and _sha256_file(dest_skill_md) == source_hash
|
|
328
|
+
except Exception:
|
|
329
|
+
dest_ok = False
|
|
330
|
+
if not dest_ok:
|
|
331
|
+
logger.warning(
|
|
332
|
+
"skill migration: cross-volume copy verification failed for %s "
|
|
333
|
+
"- rolling back destination, leaving source", source_dir,
|
|
334
|
+
)
|
|
335
|
+
shutil.rmtree(dest_dir, ignore_errors=True)
|
|
336
|
+
return False
|
|
337
|
+
finally:
|
|
338
|
+
shutil.rmtree(tmp_root, ignore_errors=True)
|
|
339
|
+
|
|
340
|
+
if cross_volume:
|
|
341
|
+
# Brief rule: cross-volume placement is copy+verify with NO source
|
|
342
|
+
# removal (a cross-device move isn't atomic, and the source tree may
|
|
343
|
+
# be read-only anyway). Leave the legacy source in place, exactly like
|
|
344
|
+
# the EACCES branch, and drop a breadcrumb at the (writable) dest.
|
|
345
|
+
logger.info(
|
|
346
|
+
"skill migration: cross-volume copy of %s to %s verified; leaving "
|
|
347
|
+
"the legacy source in place (no cross-device remove)",
|
|
348
|
+
source_dir, dest_dir,
|
|
349
|
+
)
|
|
350
|
+
try:
|
|
351
|
+
(dest_dir / _SOURCE_RETAINED_BREADCRUMB).write_text(
|
|
352
|
+
f"cross-volume copy; source not removed: {source_dir}\n", encoding="utf-8"
|
|
353
|
+
)
|
|
354
|
+
except Exception:
|
|
355
|
+
pass # breadcrumb is best-effort only - never let it fail the migration
|
|
356
|
+
return True
|
|
357
|
+
|
|
358
|
+
# Same-filesystem rename path - now best-effort remove the legacy source.
|
|
359
|
+
# Never crash or loop on a permission failure here; the data-home copy is
|
|
360
|
+
# what matters going forward regardless of whether cleanup succeeds.
|
|
361
|
+
try:
|
|
362
|
+
shutil.rmtree(source_dir)
|
|
363
|
+
except (PermissionError, OSError) as e:
|
|
364
|
+
logger.info(
|
|
365
|
+
"skill migration: copied %s to %s but could not remove the legacy "
|
|
366
|
+
"source (%s) - leaving it in place; the data-home copy is authoritative",
|
|
367
|
+
source_dir, dest_dir, e,
|
|
368
|
+
)
|
|
369
|
+
try:
|
|
370
|
+
(dest_dir / _SOURCE_RETAINED_BREADCRUMB).write_text(
|
|
371
|
+
f"source not removed: {source_dir} ({e})\n", encoding="utf-8"
|
|
372
|
+
)
|
|
373
|
+
except Exception:
|
|
374
|
+
pass # breadcrumb is best-effort only - never let it fail the migration
|
|
375
|
+
return True
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _merge_legacy_user_rules(legacy_user_dir: Path, dest_user_dir: Path) -> None:
|
|
379
|
+
"""Best-effort merge of a legacy per-user ``rules.json`` into the
|
|
380
|
+
data-home copy, filling in only keys ABSENT at the destination (an
|
|
381
|
+
existing destination entry always wins - never clobbered).
|
|
382
|
+
|
|
383
|
+
Without this, a migrated skill directory has no matching rule and
|
|
384
|
+
``SkillManager.get_skills_for_session`` never surfaces it: the body moves
|
|
385
|
+
but stays permanently inert, which would defeat the point of migrating it
|
|
386
|
+
at all. The legacy file is left in place untouched; this only enriches
|
|
387
|
+
the destination copy. Fail-open: any parse/write error is logged and
|
|
388
|
+
swallowed, never propagated.
|
|
389
|
+
"""
|
|
390
|
+
legacy_rules_file = legacy_user_dir / "rules.json"
|
|
391
|
+
if legacy_rules_file.is_symlink() or not legacy_rules_file.is_file():
|
|
392
|
+
return
|
|
393
|
+
try:
|
|
394
|
+
legacy_rules = json.loads(legacy_rules_file.read_text(encoding="utf-8"))
|
|
395
|
+
except Exception:
|
|
396
|
+
logger.debug("skill migration: could not parse legacy rules.json at %s", legacy_rules_file)
|
|
397
|
+
return
|
|
398
|
+
if not isinstance(legacy_rules, dict) or not legacy_rules:
|
|
399
|
+
return
|
|
400
|
+
|
|
401
|
+
dest_rules_file = dest_user_dir / "rules.json"
|
|
402
|
+
dest_rules: dict = {}
|
|
403
|
+
if dest_rules_file.is_file():
|
|
404
|
+
try:
|
|
405
|
+
loaded = json.loads(dest_rules_file.read_text(encoding="utf-8"))
|
|
406
|
+
except Exception:
|
|
407
|
+
logger.debug(
|
|
408
|
+
"skill migration: existing dest rules.json unparseable at %s - leaving untouched",
|
|
409
|
+
dest_rules_file,
|
|
410
|
+
)
|
|
411
|
+
return # don't risk clobbering something we can't parse
|
|
412
|
+
if isinstance(loaded, dict):
|
|
413
|
+
dest_rules = loaded
|
|
414
|
+
|
|
415
|
+
changed = False
|
|
416
|
+
for skill_id, rule in legacy_rules.items():
|
|
417
|
+
if skill_id not in dest_rules:
|
|
418
|
+
dest_rules[skill_id] = rule
|
|
419
|
+
changed = True
|
|
420
|
+
if not changed:
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
dest_user_dir.mkdir(parents=True, exist_ok=True)
|
|
424
|
+
fd, tmp_name = tempfile.mkstemp(dir=str(dest_user_dir), suffix=".tmp")
|
|
425
|
+
try:
|
|
426
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
427
|
+
json.dump(dest_rules, f, indent=2)
|
|
428
|
+
os.replace(tmp_name, str(dest_rules_file))
|
|
429
|
+
finally:
|
|
430
|
+
if os.path.exists(tmp_name):
|
|
431
|
+
try:
|
|
432
|
+
os.remove(tmp_name)
|
|
433
|
+
except OSError:
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# Dotted sibling dirs under ``user_<uid>/`` that hold real skill CONTENT and
|
|
438
|
+
# must migrate too (Task 9 review fix #2) — as opposed to bookkeeping dotted
|
|
439
|
+
# entries (``.migrate.lock``/``.migrated_v1``/``rules.json``), which are NOT
|
|
440
|
+
# skill dirs and are never walked as such. ``.pending`` = quarantined drafts
|
|
441
|
+
# awaiting review; ``.archived`` = recoverable prior/deleted bodies. Leaving
|
|
442
|
+
# these on the code tree means ``polyrob update`` destroys a user's pending
|
|
443
|
+
# drafts and their entire undo history.
|
|
444
|
+
_QUARANTINE_SUBDIRS = (".pending", ".archived")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _migrate_skill_dirs_in(src_container: Path, dest_container: Path,
|
|
448
|
+
*, label: str) -> int:
|
|
449
|
+
"""Migrate every direct child skill directory of ``src_container`` into
|
|
450
|
+
``dest_container`` using the shared per-skill pipeline + guards. Returns
|
|
451
|
+
the count placed. Never raises — a listing/per-item failure is logged and
|
|
452
|
+
skipped so one bad entry can't abort the container.
|
|
453
|
+
|
|
454
|
+
``src_container`` is either a ``user_<uid>/`` dir (top-level skills) or one
|
|
455
|
+
of its ``.pending``/``.archived`` quarantine subdirs; the identical
|
|
456
|
+
symlink / ``..`` / skip-existing / EACCES guards apply in every case.
|
|
457
|
+
"""
|
|
458
|
+
moved = 0
|
|
459
|
+
if src_container.is_symlink() or not src_container.is_dir():
|
|
460
|
+
return 0
|
|
461
|
+
try:
|
|
462
|
+
skill_dir_names = sorted(
|
|
463
|
+
d.name
|
|
464
|
+
for d in src_container.iterdir()
|
|
465
|
+
if d.is_dir()
|
|
466
|
+
and not d.is_symlink()
|
|
467
|
+
and not d.name.startswith(".")
|
|
468
|
+
and _is_safe_component(d.name)
|
|
469
|
+
and (d / "SKILL.md").is_file()
|
|
470
|
+
and not (d / "SKILL.md").is_symlink()
|
|
471
|
+
)
|
|
472
|
+
except Exception:
|
|
473
|
+
logger.debug("skill migration: failed to list %s", src_container, exc_info=True)
|
|
474
|
+
return 0
|
|
475
|
+
|
|
476
|
+
for skill_name in skill_dir_names:
|
|
477
|
+
try:
|
|
478
|
+
if _migrate_one_skill(src_container / skill_name, dest_container / skill_name):
|
|
479
|
+
moved += 1
|
|
480
|
+
except Exception:
|
|
481
|
+
logger.warning(
|
|
482
|
+
"skill migration: failed migrating %s/%s (skipped, no crash)",
|
|
483
|
+
label, skill_name, exc_info=True,
|
|
484
|
+
)
|
|
485
|
+
continue
|
|
486
|
+
return moved
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _migrate_pass(legacy_root: Path, home: Path) -> int:
|
|
490
|
+
"""Walk ``legacy_root`` for ``user_*`` dirs and migrate their skills into
|
|
491
|
+
``home`` (``skills_data_home()``). Returns the count of skill directories
|
|
492
|
+
actually placed at a new destination. Never raises - any per-item failure
|
|
493
|
+
is logged and skipped so one bad entry can't abort the whole pass.
|
|
494
|
+
|
|
495
|
+
Per ``user_<uid>/`` this migrates the active (top-level) skills AND the
|
|
496
|
+
``.pending``/``.archived`` quarantine content (all user data), plus a
|
|
497
|
+
best-effort ``rules.json`` merge.
|
|
498
|
+
"""
|
|
499
|
+
moved = 0
|
|
500
|
+
try:
|
|
501
|
+
if legacy_root.is_symlink() or not legacy_root.is_dir():
|
|
502
|
+
return 0
|
|
503
|
+
user_dir_names = sorted(
|
|
504
|
+
d.name
|
|
505
|
+
for d in legacy_root.iterdir()
|
|
506
|
+
if d.is_dir()
|
|
507
|
+
and not d.is_symlink()
|
|
508
|
+
and d.name.startswith("user_")
|
|
509
|
+
and _is_safe_component(d.name)
|
|
510
|
+
)
|
|
511
|
+
except Exception:
|
|
512
|
+
logger.debug("skill migration: failed to list legacy_root %s", legacy_root, exc_info=True)
|
|
513
|
+
return 0
|
|
514
|
+
|
|
515
|
+
for name in user_dir_names:
|
|
516
|
+
user_dir = legacy_root / name
|
|
517
|
+
dest_user_dir = home / name
|
|
518
|
+
|
|
519
|
+
# Active (top-level) skills.
|
|
520
|
+
moved += _migrate_skill_dirs_in(user_dir, dest_user_dir, label=name)
|
|
521
|
+
|
|
522
|
+
# Quarantine content (.pending drafts + .archived history) — same
|
|
523
|
+
# pipeline + guards, preserving the subdir layout in data-home.
|
|
524
|
+
for sub in _QUARANTINE_SUBDIRS:
|
|
525
|
+
moved += _migrate_skill_dirs_in(
|
|
526
|
+
user_dir / sub, dest_user_dir / sub, label=f"{name}/{sub}"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
_merge_legacy_user_rules(user_dir, dest_user_dir)
|
|
531
|
+
except Exception:
|
|
532
|
+
logger.debug("skill migration: rules.json merge skipped for %s", user_dir, exc_info=True)
|
|
533
|
+
|
|
534
|
+
return moved
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def migrate_legacy_user_skills(legacy_root: Path, *, lock_timeout: float = 5.0) -> int:
|
|
538
|
+
"""One-time migration of pre-Task-8 code-tree ``user_<uid>/`` skills into
|
|
539
|
+
data-home (Task 9). Returns the count of skill directories moved.
|
|
540
|
+
|
|
541
|
+
Safety properties (see module section header for the "why"):
|
|
542
|
+
|
|
543
|
+
- **Idempotent**: a ``<data_home>/skills/.migrated_v1`` marker is
|
|
544
|
+
written after a completed pass; any later call returns 0 immediately
|
|
545
|
+
without re-scanning ``legacy_root``.
|
|
546
|
+
- **Single-flight / concurrency-safe**: the whole pass runs under a
|
|
547
|
+
:class:`~agents.task.utils.SafeFileLock` at
|
|
548
|
+
``<data_home>/skills/.migrate.lock`` so parallel workers/processes
|
|
549
|
+
booting at once don't race each other. On contention/timeout this
|
|
550
|
+
returns 0 rather than blocking indefinitely or racing.
|
|
551
|
+
- **Resumable**: the marker is only written at the END of a pass, so an
|
|
552
|
+
interrupted first attempt (process killed mid-migration) simply
|
|
553
|
+
re-scans on the next call - already-placed destinations are detected
|
|
554
|
+
via the per-skill "skip if dest exists" check and not re-copied.
|
|
555
|
+
- **Traversal/symlink-safe**: never follows a symlinked ``user_*`` dir,
|
|
556
|
+
skill dir, or ``SKILL.md``; rejects unsafe path components.
|
|
557
|
+
- **Never clobbers**: a destination that already exists (e.g. a fresher
|
|
558
|
+
data-home copy, or one created by a concurrent writer) is left alone
|
|
559
|
+
and not counted as moved.
|
|
560
|
+
- **EACCES-tolerant**: a read-only / root-owned legacy tree (typical
|
|
561
|
+
site-packages install) means the source can't be removed after
|
|
562
|
+
copying - that failure is swallowed, the source is left in place with
|
|
563
|
+
a breadcrumb, and migration continues (never loops, never crashes).
|
|
564
|
+
- **Fail-open**: any unexpected exception anywhere in this call is
|
|
565
|
+
logged and swallowed; the count accumulated so far is returned. This
|
|
566
|
+
function must NEVER raise - it's called from ``SkillManager.__init__``
|
|
567
|
+
and must never block construction.
|
|
568
|
+
- **Zero-touch when nothing to migrate**: if ``legacy_root`` (the
|
|
569
|
+
package tree) has NO ``user_*`` dirs, returns 0 *before* resolving or
|
|
570
|
+
creating data-home / the lock / the marker — so constructing a bare
|
|
571
|
+
``SkillManager()`` on a machine that never had legacy user skills
|
|
572
|
+
never writes anything under the real ``~/.polyrob`` (or wherever
|
|
573
|
+
data-home resolves). The pre-check is on the RAW ``legacy_root``, so
|
|
574
|
+
it's environment-independent.
|
|
575
|
+
"""
|
|
576
|
+
moved = 0
|
|
577
|
+
try:
|
|
578
|
+
# Cheap, env-independent pre-check on the package tree BEFORE touching
|
|
579
|
+
# data-home (Task 9 review fix #3). No ``user_*`` dirs => nothing to
|
|
580
|
+
# migrate => return immediately, creating no lock/marker/skills dir.
|
|
581
|
+
# (Idempotency for the "had skills, already migrated" case is still the
|
|
582
|
+
# marker's job below; this only short-circuits the empty case.)
|
|
583
|
+
try:
|
|
584
|
+
has_user_dirs = any(
|
|
585
|
+
p.is_dir() and not p.is_symlink()
|
|
586
|
+
for p in legacy_root.glob("user_*")
|
|
587
|
+
)
|
|
588
|
+
except OSError:
|
|
589
|
+
has_user_dirs = False
|
|
590
|
+
if not has_user_dirs:
|
|
591
|
+
return 0
|
|
592
|
+
|
|
593
|
+
home = skills_data_home()
|
|
594
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
595
|
+
marker = home / _MIGRATION_MARKER_NAME
|
|
596
|
+
if marker.exists():
|
|
597
|
+
return 0
|
|
598
|
+
|
|
599
|
+
from agents.task.utils import SafeFileLock
|
|
600
|
+
|
|
601
|
+
lock_path = home / _MIGRATION_LOCK_NAME
|
|
602
|
+
try:
|
|
603
|
+
with SafeFileLock(str(lock_path), timeout=lock_timeout):
|
|
604
|
+
if marker.exists():
|
|
605
|
+
# Another process finished while we waited for the lock.
|
|
606
|
+
return 0
|
|
607
|
+
# A missing/symlinked legacy_root means we never actually got
|
|
608
|
+
# to inspect anything - don't mark the migration "done" in
|
|
609
|
+
# that case, so a legacy tree that appears later (unusual,
|
|
610
|
+
# but cheap to stay open to) can still be picked up.
|
|
611
|
+
root_was_inspectable = legacy_root.is_dir() and not legacy_root.is_symlink()
|
|
612
|
+
moved = _migrate_pass(legacy_root, home)
|
|
613
|
+
if not root_was_inspectable:
|
|
614
|
+
return moved
|
|
615
|
+
try:
|
|
616
|
+
marker.write_text(f"migrated {moved} skill(s)\n", encoding="utf-8")
|
|
617
|
+
except Exception:
|
|
618
|
+
logger.debug(
|
|
619
|
+
"skill migration: could not write completion marker "
|
|
620
|
+
"(will re-scan on next boot)", exc_info=True,
|
|
621
|
+
)
|
|
622
|
+
except TimeoutError:
|
|
623
|
+
logger.info("skill migration: lock at %s contended - skipping this attempt", lock_path)
|
|
624
|
+
return 0
|
|
625
|
+
except Exception:
|
|
626
|
+
logger.debug("skill migration: unexpected error - skipping (fail-open)", exc_info=True)
|
|
627
|
+
return moved
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
ALLOWED_TOP = {"name", "description", "license", "compatibility", "metadata", "allowed-tools"}
|
|
5
|
+
_NAME_STRICT = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$") # ascii kebab, no --, no lead/trail -
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Issue:
|
|
9
|
+
level: str # "error" | "warn"
|
|
10
|
+
code: str
|
|
11
|
+
msg: str
|
|
12
|
+
|
|
13
|
+
def validate_authored(meta: dict, dirname: str) -> list[Issue]:
|
|
14
|
+
out: list[Issue] = []
|
|
15
|
+
name = meta.get("name")
|
|
16
|
+
if not name:
|
|
17
|
+
out.append(Issue("error", "missing_name", "name is required"))
|
|
18
|
+
elif not isinstance(name, str):
|
|
19
|
+
out.append(Issue("error", "name_not_string", "name must be a string"))
|
|
20
|
+
else:
|
|
21
|
+
if len(name) > 64:
|
|
22
|
+
out.append(Issue("error", "name_too_long", "name >64 chars"))
|
|
23
|
+
if not _NAME_STRICT.match(name):
|
|
24
|
+
out.append(Issue("error", "name_charset", "name must be ^[a-z0-9]+(-[a-z0-9]+)*$"))
|
|
25
|
+
if name != dirname:
|
|
26
|
+
out.append(Issue("error", "name_dir_mismatch", f"name {name!r} != dir {dirname!r}"))
|
|
27
|
+
desc = meta.get("description")
|
|
28
|
+
if not desc:
|
|
29
|
+
out.append(Issue("error", "missing_description", "description is required"))
|
|
30
|
+
elif not isinstance(desc, str):
|
|
31
|
+
out.append(Issue("error", "description_not_string", "description must be a string"))
|
|
32
|
+
elif len(desc) > 1024:
|
|
33
|
+
out.append(Issue("error", "description_too_long", "description >1024 chars"))
|
|
34
|
+
extra = set(meta.keys()) - ALLOWED_TOP
|
|
35
|
+
if extra:
|
|
36
|
+
out.append(Issue("error", "extra_top_level_field", f"unexpected top-level: {sorted(extra)}"))
|
|
37
|
+
comp = meta.get("compatibility")
|
|
38
|
+
if comp and len(str(comp)) > 500:
|
|
39
|
+
out.append(Issue("error", "compatibility_too_long", "compatibility >500 chars"))
|
|
40
|
+
md = meta.get("metadata")
|
|
41
|
+
if md is not None and (not isinstance(md, dict) or any(not isinstance(k, str) or not isinstance(v, str) for k, v in md.items())):
|
|
42
|
+
out.append(Issue("error", "metadata_not_string_map", "metadata must be string->string"))
|
|
43
|
+
return out
|
|
44
|
+
|
|
45
|
+
def validate_consumed(meta: dict, dirname: str) -> list[Issue]:
|
|
46
|
+
out: list[Issue] = []
|
|
47
|
+
name = meta.get("name")
|
|
48
|
+
if not meta.get("description"):
|
|
49
|
+
out.append(Issue("error", "missing_description", "description required to load")) # skip
|
|
50
|
+
if name and not isinstance(name, str):
|
|
51
|
+
out.append(Issue("warn", "name_not_string", "name is not a string (loaded anyway)"))
|
|
52
|
+
elif name and len(name) > 64:
|
|
53
|
+
out.append(Issue("warn", "name_too_long", "name >64 (loaded anyway)"))
|
|
54
|
+
if name and isinstance(name, str) and name != dirname:
|
|
55
|
+
out.append(Issue("warn", "name_dir_mismatch", "name!=dir (loaded anyway)"))
|
|
56
|
+
return out
|