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,492 @@
|
|
|
1
|
+
"""Writable skills (W2-A, Reference-parity self-authored skills).
|
|
2
|
+
|
|
3
|
+
POLYROB's :class:`SkillManager` was read-only — the agent could *match* and *load* skills
|
|
4
|
+
but never create, refine, or retire them, so it could not learn durable procedures
|
|
5
|
+
from experience. This mixin adds the write path, composed into ``SkillManager`` so
|
|
6
|
+
all the existing validators/loaders are reused.
|
|
7
|
+
|
|
8
|
+
Safety is designed in, not bolted on — a self-authored skill is a prompt-injection
|
|
9
|
+
**persistence** vector (a skill loaded next session is read as instructions):
|
|
10
|
+
|
|
11
|
+
1. **Tenant-confined** — writes go ONLY under the user DATA-HOME scope's
|
|
12
|
+
``user_{user_id}/`` (Task 8: ``skill_store.skills_data_home()``, NOT the
|
|
13
|
+
installed package tree — this is what lets a self-authored skill survive a
|
|
14
|
+
``polyrob update`` code-swap); the ``^[a-z][a-z0-9-]*$`` id regex
|
|
15
|
+
(``validate_skill_id``) blocks path traversal.
|
|
16
|
+
2. **Anon-blocked** — an empty/blank ``user_id`` is refused (mirror
|
|
17
|
+
``MEMORY_REQUIRE_USER_ID``) so authored skills never land in a shared bucket.
|
|
18
|
+
3. **Validated** — ``validate_skill_id`` → ``validate_skill_content``
|
|
19
|
+
(``MAX_SKILL_CONTENT_CHARS``) before any write.
|
|
20
|
+
4. **Threat-scanned** — ``threat_scan.is_suspicious`` rejects obviously-injected
|
|
21
|
+
bodies BEFORE persist (unconditional on the write path here — a read-only
|
|
22
|
+
trusted scope, i.e. builtin, is exempt via ``skill_store.scan_exempt``, but
|
|
23
|
+
`create_skill` only ever targets the writable user scope, so this is
|
|
24
|
+
effectively still unconditional; not gated on the memory flag).
|
|
25
|
+
5. **Quarantined** — when ``SKILLS_WRITABLE_REQUIRE_REVIEW`` (default true) OR the
|
|
26
|
+
author was a non-user-initiated turn (self-wake / background review), the skill
|
|
27
|
+
lands in ``user_{uid}/.pending/`` **inert** (NOT in rules.json), so a forged turn
|
|
28
|
+
can never auto-activate a skill.
|
|
29
|
+
6. **Atomic** — temp-file + ``os.replace`` so a crash never leaves a half-written
|
|
30
|
+
``SKILL.md``.
|
|
31
|
+
7. **Archive-never-delete** — deletes move to ``user_{uid}/.archived/`` (recoverable).
|
|
32
|
+
|
|
33
|
+
Gated by ``SKILLS_WRITABLE`` at the action layer; the methods here are a library and
|
|
34
|
+
do nothing until called.
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
import logging
|
|
40
|
+
import os
|
|
41
|
+
import tempfile
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import List, Optional
|
|
44
|
+
|
|
45
|
+
from agents.task.agent import skill_store
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
PROVENANCE_USER = "user"
|
|
50
|
+
PROVENANCE_AGENT = "agent"
|
|
51
|
+
PROVENANCE_BACKGROUND = "background_review"
|
|
52
|
+
# Authors whose writes must NEVER auto-activate, regardless of REQUIRE_REVIEW: a
|
|
53
|
+
# forged (self-wake / background-review) turn. A normal interactive `agent` turn is
|
|
54
|
+
# user-initiated, so it follows the REQUIRE_REVIEW flag like any other.
|
|
55
|
+
_NON_USER_AUTHORS = frozenset({PROVENANCE_BACKGROUND})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SkillWriteResult:
|
|
59
|
+
"""Outcome of a write op (mirrors SkillValidationResult's shape, plus location)."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, skill_id: str, ok: bool, *, errors=None, warnings=None,
|
|
62
|
+
pending: bool = False, path: Optional[str] = None):
|
|
63
|
+
self.skill_id = skill_id
|
|
64
|
+
self.is_valid = ok
|
|
65
|
+
self.ok = ok
|
|
66
|
+
self.errors = errors or []
|
|
67
|
+
self.warnings = warnings or []
|
|
68
|
+
self.pending = pending
|
|
69
|
+
self.path = path
|
|
70
|
+
|
|
71
|
+
def __repr__(self):
|
|
72
|
+
state = "pending" if self.pending else ("ok" if self.ok else "rejected")
|
|
73
|
+
return f"<SkillWriteResult {self.skill_id} {state} errors={self.errors}>"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SkillWriterMixin:
|
|
77
|
+
"""Create / patch / delete user skills. Composed into SkillManager."""
|
|
78
|
+
|
|
79
|
+
def _user_root(self, user_id: str) -> Path:
|
|
80
|
+
# Task 8: routes through SkillManager._user_dirs_root() (data-home by
|
|
81
|
+
# default; the pre-existing single-root test-override contract if
|
|
82
|
+
# `skills_dir` was redirected) rather than always `self.skills_dir` —
|
|
83
|
+
# so a create/patch/delete survives a `polyrob update` code-swap.
|
|
84
|
+
return self._user_dirs_root() / f"user_{user_id}"
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _require_user(user_id: Optional[str]) -> Optional[str]:
|
|
88
|
+
"""Return a clean user_id or None (anon-block). Never write under user_/."""
|
|
89
|
+
if user_id is None:
|
|
90
|
+
return None
|
|
91
|
+
uid = str(user_id).strip()
|
|
92
|
+
return uid or None
|
|
93
|
+
|
|
94
|
+
def _resolve_pending(self, created_by: str, pending: Optional[bool],
|
|
95
|
+
*, overwriting_active: bool = False) -> bool:
|
|
96
|
+
from agents.task.constants import AutonomyConfig
|
|
97
|
+
# Protect curated ACTIVE skills: a non-owner author (agent or background) may
|
|
98
|
+
# never silently replace one — force a .pending proposal (owner promotes).
|
|
99
|
+
if (overwriting_active and created_by != PROVENANCE_USER
|
|
100
|
+
and AutonomyConfig.skill_overwrite_protect()):
|
|
101
|
+
return True
|
|
102
|
+
if pending is not None:
|
|
103
|
+
base = pending
|
|
104
|
+
else:
|
|
105
|
+
base = AutonomyConfig.skills_writable_require_review()
|
|
106
|
+
# Hard rule: a non-user author can NEVER auto-activate — force quarantine even
|
|
107
|
+
# if review is globally disabled.
|
|
108
|
+
if created_by in _NON_USER_AUTHORS:
|
|
109
|
+
return True
|
|
110
|
+
return base
|
|
111
|
+
|
|
112
|
+
def create_skill(self, skill_id: str, content: str, *, user_id: str,
|
|
113
|
+
description: str = "", created_by: str = PROVENANCE_AGENT,
|
|
114
|
+
pending: Optional[bool] = None) -> SkillWriteResult:
|
|
115
|
+
"""Author a new user skill (validated, threat-scanned, atomically written)."""
|
|
116
|
+
uid = self._require_user(user_id)
|
|
117
|
+
if uid is None:
|
|
118
|
+
return SkillWriteResult(skill_id, False,
|
|
119
|
+
errors=["empty user_id refused (tenant scope)"])
|
|
120
|
+
|
|
121
|
+
id_ok, id_errors = self.validate_skill_id(skill_id)
|
|
122
|
+
if not id_ok:
|
|
123
|
+
return SkillWriteResult(skill_id, False, errors=id_errors)
|
|
124
|
+
|
|
125
|
+
content_res = self.validate_skill_content(skill_id, content)
|
|
126
|
+
if not content_res.is_valid:
|
|
127
|
+
return SkillWriteResult(skill_id, False, errors=content_res.errors,
|
|
128
|
+
warnings=content_res.warnings)
|
|
129
|
+
|
|
130
|
+
# Threat-scan is the injection-persistence tripwire. Fail-OPEN only on an
|
|
131
|
+
# import error (scanner absent); if the scan itself RAISES we fail-CLOSED
|
|
132
|
+
# (reject) — a write must not slip past a crashing guard.
|
|
133
|
+
try:
|
|
134
|
+
from modules.memory.task.threat_scan import is_suspicious
|
|
135
|
+
except Exception:
|
|
136
|
+
is_suspicious = None
|
|
137
|
+
# Task 8: a read-only + trusted scope (today: builtin) is exempt from a
|
|
138
|
+
# write-time re-scan — future-proofs e.g. a reindex of the shipped
|
|
139
|
+
# library. `create_skill` ALWAYS targets the writable user scope
|
|
140
|
+
# (`skill_store.user_scope()`, writable=True), so `scan_exempt()` is
|
|
141
|
+
# unconditionally False here — this must NEVER weaken scanning on the
|
|
142
|
+
# live (user) write path, regardless of user_scope() also being
|
|
143
|
+
# `trusted` (trusted describes location provenance, not a license to
|
|
144
|
+
# skip scanning newly-written content).
|
|
145
|
+
_write_scope = skill_store.user_scope()
|
|
146
|
+
if is_suspicious is not None and not skill_store.scan_exempt(_write_scope):
|
|
147
|
+
try:
|
|
148
|
+
flagged = is_suspicious(content)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.warning("skill write rejected (threat-scan error, fail-closed): %s/%s: %s",
|
|
151
|
+
uid, skill_id, e)
|
|
152
|
+
return SkillWriteResult(skill_id, False, errors=["threat-scan error (rejected)"])
|
|
153
|
+
if flagged:
|
|
154
|
+
logger.warning("skill write rejected (suspicious content): %s/%s", uid, skill_id)
|
|
155
|
+
return SkillWriteResult(skill_id, False,
|
|
156
|
+
errors=["content failed injection threat-scan"])
|
|
157
|
+
# P3-1: the DESCRIPTION is injected verbatim into the <skill-catalog>
|
|
158
|
+
# prompt for every future session — so it is an injection vector exactly
|
|
159
|
+
# like the body and must be scanned too (latent Hermes #8884). Fail-CLOSED.
|
|
160
|
+
if description:
|
|
161
|
+
try:
|
|
162
|
+
desc_flagged = is_suspicious(description)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.warning("skill write rejected (description scan error, fail-closed): %s/%s: %s",
|
|
165
|
+
uid, skill_id, e)
|
|
166
|
+
return SkillWriteResult(skill_id, False,
|
|
167
|
+
errors=["description scan error (rejected)"])
|
|
168
|
+
if desc_flagged:
|
|
169
|
+
logger.warning("skill write rejected (suspicious description): %s/%s", uid, skill_id)
|
|
170
|
+
return SkillWriteResult(skill_id, False,
|
|
171
|
+
errors=["description failed injection threat-scan"])
|
|
172
|
+
|
|
173
|
+
active_file = self._user_root(uid) / skill_id / "SKILL.md"
|
|
174
|
+
overwriting_active = active_file.exists() or (skill_id in getattr(self, "skill_rules", {}))
|
|
175
|
+
quarantine = self._resolve_pending(created_by, pending,
|
|
176
|
+
overwriting_active=overwriting_active)
|
|
177
|
+
base = self._user_root(uid) / (".pending" if quarantine else "")
|
|
178
|
+
skill_dir = base / skill_id
|
|
179
|
+
skill_file = skill_dir / "SKILL.md"
|
|
180
|
+
try:
|
|
181
|
+
if skill_file.exists():
|
|
182
|
+
self._archive_prior_body(uid, skill_id, skill_file) # non-destructive overwrite
|
|
183
|
+
self._atomic_write(skill_file, content)
|
|
184
|
+
if not quarantine:
|
|
185
|
+
self._upsert_rule(uid, skill_id, description=description,
|
|
186
|
+
created_by=created_by, content=content)
|
|
187
|
+
self._invalidate_skill_cache(uid, skill_id)
|
|
188
|
+
self._record_provenance(skill_id, uid, created_by)
|
|
189
|
+
logger.info("authored skill %s/%s (%s)", uid, skill_id,
|
|
190
|
+
"pending" if quarantine else "active")
|
|
191
|
+
return SkillWriteResult(skill_id, True, pending=quarantine,
|
|
192
|
+
warnings=content_res.warnings, path=str(skill_file))
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.error("skill write failed for %s/%s: %s", uid, skill_id, e, exc_info=True)
|
|
195
|
+
return SkillWriteResult(skill_id, False, errors=[f"write failed: {e}"])
|
|
196
|
+
|
|
197
|
+
def patch_skill(self, skill_id: str, *, user_id: str, old_string: str,
|
|
198
|
+
new_string: str, replace_all: bool = False,
|
|
199
|
+
created_by: str = PROVENANCE_AGENT,
|
|
200
|
+
pending: Optional[bool] = None) -> SkillWriteResult:
|
|
201
|
+
"""Exact-match edit of an existing user skill body, re-validated + re-scanned."""
|
|
202
|
+
uid = self._require_user(user_id)
|
|
203
|
+
if uid is None:
|
|
204
|
+
return SkillWriteResult(skill_id, False, errors=["empty user_id refused"])
|
|
205
|
+
id_ok, id_errors = self.validate_skill_id(skill_id)
|
|
206
|
+
if not id_ok:
|
|
207
|
+
return SkillWriteResult(skill_id, False, errors=id_errors)
|
|
208
|
+
|
|
209
|
+
skill_file = self._find_skill_file(uid, skill_id)
|
|
210
|
+
if skill_file is None:
|
|
211
|
+
return SkillWriteResult(skill_id, False, errors=[f"skill '{skill_id}' not found for user"])
|
|
212
|
+
# A forged (non-user) turn may refine its OWN pending draft but must never
|
|
213
|
+
# mutate an already-ACTIVE skill (that's user-controlled instruction content).
|
|
214
|
+
if created_by in _NON_USER_AUTHORS and not self._is_pending_path(skill_file):
|
|
215
|
+
return SkillWriteResult(skill_id, False,
|
|
216
|
+
errors=["a background turn cannot patch an active skill"])
|
|
217
|
+
try:
|
|
218
|
+
current = skill_file.read_text(encoding="utf-8")
|
|
219
|
+
except Exception as e:
|
|
220
|
+
return SkillWriteResult(skill_id, False, errors=[f"read failed: {e}"])
|
|
221
|
+
|
|
222
|
+
count = current.count(old_string)
|
|
223
|
+
if count == 0:
|
|
224
|
+
return SkillWriteResult(skill_id, False, errors=["old_string not found"])
|
|
225
|
+
if count > 1 and not replace_all:
|
|
226
|
+
return SkillWriteResult(skill_id, False,
|
|
227
|
+
errors=[f"old_string occurs {count}× — pass replace_all=true"])
|
|
228
|
+
updated = (current.replace(old_string, new_string)
|
|
229
|
+
if replace_all else current.replace(old_string, new_string, 1))
|
|
230
|
+
|
|
231
|
+
# Re-run the full validate+scan gate on the NEW body.
|
|
232
|
+
return self.create_skill(skill_id, updated, user_id=uid, created_by=created_by,
|
|
233
|
+
pending=pending if pending is not None else self._is_pending_path(skill_file))
|
|
234
|
+
|
|
235
|
+
def delete_skill(self, skill_id: str, *, user_id: str,
|
|
236
|
+
absorbed_into: Optional[str] = None,
|
|
237
|
+
created_by: str = PROVENANCE_USER) -> bool:
|
|
238
|
+
"""Archive a user skill (recoverable). Never hard-deletes; drops its rule."""
|
|
239
|
+
uid = self._require_user(user_id)
|
|
240
|
+
if uid is None:
|
|
241
|
+
return False
|
|
242
|
+
id_ok, _ = self.validate_skill_id(skill_id)
|
|
243
|
+
if not id_ok:
|
|
244
|
+
return False
|
|
245
|
+
skill_file = self._find_skill_file(uid, skill_id)
|
|
246
|
+
if skill_file is None:
|
|
247
|
+
return False
|
|
248
|
+
# A forged (non-user) turn must never retire an ACTIVE skill.
|
|
249
|
+
if created_by in _NON_USER_AUTHORS and not self._is_pending_path(skill_file):
|
|
250
|
+
logger.info("background turn blocked from deleting active skill %s/%s", uid, skill_id)
|
|
251
|
+
return False
|
|
252
|
+
try:
|
|
253
|
+
archive_dir = self._user_root(uid) / ".archived" / skill_id
|
|
254
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
255
|
+
dest = archive_dir / "SKILL.md"
|
|
256
|
+
os.replace(str(skill_file), str(dest))
|
|
257
|
+
if absorbed_into:
|
|
258
|
+
(archive_dir / "ABSORBED_INTO").write_text(absorbed_into, encoding="utf-8")
|
|
259
|
+
self._remove_rule(uid, skill_id)
|
|
260
|
+
self.reload_rules()
|
|
261
|
+
logger.info("archived skill %s/%s%s", uid, skill_id,
|
|
262
|
+
f" (absorbed into {absorbed_into})" if absorbed_into else "")
|
|
263
|
+
return True
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.error("skill archive failed for %s/%s: %s", uid, skill_id, e, exc_info=True)
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
def promote_pending_skill(self, skill_id: str, *, user_id: str,
|
|
269
|
+
description: str = "") -> SkillWriteResult:
|
|
270
|
+
"""Move a `.pending/` skill into active use (the human/curator review gate)."""
|
|
271
|
+
uid = self._require_user(user_id)
|
|
272
|
+
if uid is None:
|
|
273
|
+
return SkillWriteResult(skill_id, False, errors=["empty user_id refused"])
|
|
274
|
+
id_ok, id_errors = self.validate_skill_id(skill_id)
|
|
275
|
+
if not id_ok:
|
|
276
|
+
return SkillWriteResult(skill_id, False, errors=id_errors)
|
|
277
|
+
pending_file = self._user_root(uid) / ".pending" / skill_id / "SKILL.md"
|
|
278
|
+
if not pending_file.exists():
|
|
279
|
+
return SkillWriteResult(skill_id, False, errors=["no pending skill with that id"])
|
|
280
|
+
content = pending_file.read_text(encoding="utf-8")
|
|
281
|
+
# P2-20: read the ORIGINAL author before we re-create. Promote must ACTIVATE the
|
|
282
|
+
# skill (that's the review gate), so we still create it as PROVENANCE_USER —
|
|
283
|
+
# passing the original author (e.g. background_review) would make create_skill's
|
|
284
|
+
# quarantine logic re-quarantine it and promote would never activate. Instead we
|
|
285
|
+
# re-record the original authorship in provenance AFTER activation, so a promoted
|
|
286
|
+
# skill stays ACTIVE yet remains in the curator's authored scope AND the
|
|
287
|
+
# authored-reuse metric (where a successfully promoted agent skill should count).
|
|
288
|
+
_orig_author = None
|
|
289
|
+
try:
|
|
290
|
+
from modules.skills.skill_usage import get_skill_usage_store
|
|
291
|
+
_prior = get_skill_usage_store().get_provenance(skill_id, uid)
|
|
292
|
+
if _prior and _prior.get("created_by"):
|
|
293
|
+
_orig_author = _prior["created_by"]
|
|
294
|
+
except Exception:
|
|
295
|
+
_orig_author = None
|
|
296
|
+
res = self.create_skill(skill_id, content, user_id=uid, description=description,
|
|
297
|
+
created_by=PROVENANCE_USER, pending=False)
|
|
298
|
+
if res.ok and not res.pending and _orig_author and _orig_author != PROVENANCE_USER:
|
|
299
|
+
try:
|
|
300
|
+
from modules.skills.skill_usage import get_skill_usage_store
|
|
301
|
+
get_skill_usage_store().record_provenance(skill_id, uid, _orig_author)
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
if res.ok and not res.pending:
|
|
305
|
+
try:
|
|
306
|
+
os.remove(str(pending_file))
|
|
307
|
+
except OSError:
|
|
308
|
+
pass
|
|
309
|
+
return res
|
|
310
|
+
|
|
311
|
+
def list_pending_skills(self, user_id: str) -> List[dict]:
|
|
312
|
+
"""Enumerate a tenant's `.pending/` skill drafts (owner transparency surface)."""
|
|
313
|
+
uid = self._require_user(user_id)
|
|
314
|
+
if uid is None:
|
|
315
|
+
return []
|
|
316
|
+
pending_root = self._user_root(uid) / ".pending"
|
|
317
|
+
if not pending_root.is_dir():
|
|
318
|
+
return []
|
|
319
|
+
out: List[dict] = []
|
|
320
|
+
for skill_dir in sorted(pending_root.iterdir()):
|
|
321
|
+
skill_file = skill_dir / "SKILL.md"
|
|
322
|
+
if not skill_file.is_file():
|
|
323
|
+
continue
|
|
324
|
+
try:
|
|
325
|
+
body = skill_file.read_text(encoding="utf-8")
|
|
326
|
+
except Exception:
|
|
327
|
+
continue
|
|
328
|
+
preview = body.strip().replace("\n", " ")
|
|
329
|
+
if len(preview) > 280:
|
|
330
|
+
preview = preview[:277] + "…"
|
|
331
|
+
out.append({
|
|
332
|
+
"kind": "skill",
|
|
333
|
+
"skill_id": skill_dir.name,
|
|
334
|
+
"user_id": uid,
|
|
335
|
+
"preview": preview,
|
|
336
|
+
"chars": len(body),
|
|
337
|
+
"path": str(skill_file),
|
|
338
|
+
})
|
|
339
|
+
return out
|
|
340
|
+
|
|
341
|
+
def reject_pending_skill(self, skill_id: str, *, user_id: str) -> bool:
|
|
342
|
+
"""Discard a `.pending/` skill draft (owner rejects). Archive-never-delete;
|
|
343
|
+
never touches an active skill (a non-pending id is refused)."""
|
|
344
|
+
uid = self._require_user(user_id)
|
|
345
|
+
if uid is None:
|
|
346
|
+
return False
|
|
347
|
+
id_ok, _ = self.validate_skill_id(skill_id)
|
|
348
|
+
if not id_ok:
|
|
349
|
+
return False
|
|
350
|
+
pending_file = self._user_root(uid) / ".pending" / skill_id / "SKILL.md"
|
|
351
|
+
if not pending_file.exists():
|
|
352
|
+
return False
|
|
353
|
+
try:
|
|
354
|
+
archive_dir = self._user_root(uid) / ".archived" / skill_id
|
|
355
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
356
|
+
os.replace(str(pending_file), str(archive_dir / "SKILL.md"))
|
|
357
|
+
logger.info("rejected pending skill %s/%s (archived)", uid, skill_id)
|
|
358
|
+
return True
|
|
359
|
+
except Exception as e:
|
|
360
|
+
logger.error("skill reject failed for %s/%s: %s", uid, skill_id, e, exc_info=True)
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
# --- internals -----------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
def _archive_prior_body(self, uid: str, skill_id: str, current_file: Path) -> None:
|
|
366
|
+
"""Copy the body we're about to overwrite into .archived/ (recoverable). Fail-open."""
|
|
367
|
+
try:
|
|
368
|
+
archive_dir = self._user_root(uid) / ".archived" / skill_id
|
|
369
|
+
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
370
|
+
n = 1 + sum(1 for _ in archive_dir.glob("*-SKILL.md"))
|
|
371
|
+
dest = archive_dir / f"{n}-SKILL.md"
|
|
372
|
+
content = current_file.read_text(encoding="utf-8")
|
|
373
|
+
if dest.exists():
|
|
374
|
+
# Collision-safe fallback: use a unique temp name in the same dir.
|
|
375
|
+
fd, _tmp = tempfile.mkstemp(prefix=f"{n}-", suffix="-SKILL.md", dir=str(archive_dir))
|
|
376
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
377
|
+
f.write(content)
|
|
378
|
+
else:
|
|
379
|
+
dest.write_text(content, encoding="utf-8")
|
|
380
|
+
except Exception as e:
|
|
381
|
+
logger.debug("archive-on-overwrite skipped for %s/%s: %s", uid, skill_id, e)
|
|
382
|
+
|
|
383
|
+
def _atomic_write(self, path: Path, content: str) -> None:
|
|
384
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
385
|
+
fd, tmp = tempfile.mkstemp(dir=str(path.parent), suffix=".tmp")
|
|
386
|
+
try:
|
|
387
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
388
|
+
f.write(content)
|
|
389
|
+
os.replace(tmp, str(path))
|
|
390
|
+
finally:
|
|
391
|
+
if os.path.exists(tmp):
|
|
392
|
+
try:
|
|
393
|
+
os.remove(tmp)
|
|
394
|
+
except OSError:
|
|
395
|
+
pass
|
|
396
|
+
|
|
397
|
+
def _find_skill_file(self, uid: str, skill_id: str) -> Optional[Path]:
|
|
398
|
+
# Defense-in-depth: never join an unvalidated id into a path (callers also
|
|
399
|
+
# validate, but a bad id must never traverse out of user_{uid}/).
|
|
400
|
+
ok, _ = self.validate_skill_id(skill_id)
|
|
401
|
+
if not ok:
|
|
402
|
+
return None
|
|
403
|
+
for sub in ("", ".pending"):
|
|
404
|
+
f = self._user_root(uid) / sub / skill_id / "SKILL.md" if sub else \
|
|
405
|
+
self._user_root(uid) / skill_id / "SKILL.md"
|
|
406
|
+
if f.exists():
|
|
407
|
+
return f
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
def _is_pending_path(self, path: Path) -> bool:
|
|
411
|
+
return ".pending" in path.parts
|
|
412
|
+
|
|
413
|
+
def _user_rules_path(self, uid: str) -> Path:
|
|
414
|
+
return self._user_root(uid) / "rules.json"
|
|
415
|
+
|
|
416
|
+
def _load_user_rules_raw(self, uid: str) -> dict:
|
|
417
|
+
p = self._user_rules_path(uid)
|
|
418
|
+
if p.exists():
|
|
419
|
+
try:
|
|
420
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
421
|
+
except Exception:
|
|
422
|
+
return {}
|
|
423
|
+
return {}
|
|
424
|
+
|
|
425
|
+
def _upsert_rule(self, uid: str, skill_id: str, *, description: str, created_by: str,
|
|
426
|
+
content: str = "") -> None:
|
|
427
|
+
rules = self._load_user_rules_raw(uid)
|
|
428
|
+
entry = rules.get(skill_id, {}) if isinstance(rules.get(skill_id), dict) else {}
|
|
429
|
+
existing_triggers = entry.get("triggers") if isinstance(entry.get("triggers"), dict) else {}
|
|
430
|
+
# An authored skill MUST be match-eligible or it is a dead write: get_skills_for_session
|
|
431
|
+
# skips rules with auto_activate falsey, and only MATCHED skills enter the on-demand
|
|
432
|
+
# catalog/_session_skills that load_skill resolves from. So set auto_activate=True and
|
|
433
|
+
# derive keyword triggers from the id/description so the skill surfaces when relevant.
|
|
434
|
+
entry.update({
|
|
435
|
+
"description": description or entry.get("description", ""),
|
|
436
|
+
"created_by": created_by,
|
|
437
|
+
"auto_activate": True,
|
|
438
|
+
"priority": entry.get("priority", 6), # below curated system skills (1-5)
|
|
439
|
+
"triggers": existing_triggers or {
|
|
440
|
+
"keywords": self._derive_keywords(skill_id, description, content),
|
|
441
|
+
},
|
|
442
|
+
})
|
|
443
|
+
rules[skill_id] = entry
|
|
444
|
+
self._atomic_write(self._user_rules_path(uid), json.dumps(rules, indent=2))
|
|
445
|
+
|
|
446
|
+
@staticmethod
|
|
447
|
+
def _derive_keywords(skill_id: str, description: str, content: str) -> List[str]:
|
|
448
|
+
"""Keyword triggers so an authored skill matches relevant future tasks.
|
|
449
|
+
|
|
450
|
+
Uses the skill-id tokens + significant words from the description/first heading.
|
|
451
|
+
Conservative (no giant keyword lists) to avoid over-triggering.
|
|
452
|
+
"""
|
|
453
|
+
import re as _re
|
|
454
|
+
stop = {"the", "and", "for", "with", "this", "that", "your", "you", "when",
|
|
455
|
+
"how", "use", "using", "a", "an", "to", "of", "in", "on", "is", "it",
|
|
456
|
+
"skill", "do", "doing", "make", "making", "create", "creating",
|
|
457
|
+
# P3-4: too-generic tokens that over-trigger under substring matching.
|
|
458
|
+
"data", "file", "files", "report", "info", "list", "page", "tool"}
|
|
459
|
+
words: List[str] = [t for t in skill_id.split("-") if len(t) > 2 and t not in stop]
|
|
460
|
+
src = f"{description}\n{content[:200]}".lower()
|
|
461
|
+
for tok in _re.findall(r"[a-z][a-z0-9]{3,}", src):
|
|
462
|
+
if tok not in stop and tok not in words:
|
|
463
|
+
words.append(tok)
|
|
464
|
+
if len(words) >= 5:
|
|
465
|
+
break
|
|
466
|
+
return words[:5]
|
|
467
|
+
|
|
468
|
+
def _invalidate_skill_cache(self, uid: str, skill_id: str) -> None:
|
|
469
|
+
cache = getattr(self, "skill_cache", None)
|
|
470
|
+
if isinstance(cache, dict):
|
|
471
|
+
cache.pop(f"{uid}:{skill_id}", None)
|
|
472
|
+
cache.pop(skill_id, None)
|
|
473
|
+
# Sibling of skill_cache (Task 5): the parsed-frontmatter cache must be
|
|
474
|
+
# invalidated in lockstep, else a stale meta['description'] could survive
|
|
475
|
+
# an overwrite and keep surfacing in the catalog.
|
|
476
|
+
meta_cache = getattr(self, "skill_meta_cache", None)
|
|
477
|
+
if isinstance(meta_cache, dict):
|
|
478
|
+
meta_cache.pop(f"{uid}:{skill_id}", None)
|
|
479
|
+
meta_cache.pop(skill_id, None)
|
|
480
|
+
|
|
481
|
+
def _remove_rule(self, uid: str, skill_id: str) -> None:
|
|
482
|
+
rules = self._load_user_rules_raw(uid)
|
|
483
|
+
if skill_id in rules:
|
|
484
|
+
rules.pop(skill_id, None)
|
|
485
|
+
self._atomic_write(self._user_rules_path(uid), json.dumps(rules, indent=2))
|
|
486
|
+
|
|
487
|
+
def _record_provenance(self, skill_id: str, uid: str, created_by: str) -> None:
|
|
488
|
+
try:
|
|
489
|
+
from modules.skills.skill_usage import get_skill_usage_store
|
|
490
|
+
get_skill_usage_store().record_provenance(skill_id, uid, created_by)
|
|
491
|
+
except Exception:
|
|
492
|
+
pass # provenance is a metric, never block a write on it
|