code-puppy-core-plugins 0.0.2__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.
- code_puppy_core_plugins/__init__.py +5 -0
- code_puppy_core_plugins/acp/README.md +266 -0
- code_puppy_core_plugins/acp/__init__.py +8 -0
- code_puppy_core_plugins/acp/agent.py +494 -0
- code_puppy_core_plugins/acp/bridge.py +309 -0
- code_puppy_core_plugins/acp/capabilities.py +73 -0
- code_puppy_core_plugins/acp/commands.py +116 -0
- code_puppy_core_plugins/acp/content.py +115 -0
- code_puppy_core_plugins/acp/io_delegation.py +231 -0
- code_puppy_core_plugins/acp/mcp_config.py +66 -0
- code_puppy_core_plugins/acp/permissions.py +159 -0
- code_puppy_core_plugins/acp/persistence.py +197 -0
- code_puppy_core_plugins/acp/register_callbacks.py +110 -0
- code_puppy_core_plugins/acp/replay.py +114 -0
- code_puppy_core_plugins/acp/session.py +329 -0
- code_puppy_core_plugins/acp/session_config.py +179 -0
- code_puppy_core_plugins/acp/state.py +135 -0
- code_puppy_core_plugins/agent_skills/__init__.py +22 -0
- code_puppy_core_plugins/agent_skills/config.py +208 -0
- code_puppy_core_plugins/agent_skills/discovery.py +337 -0
- code_puppy_core_plugins/agent_skills/downloader.py +392 -0
- code_puppy_core_plugins/agent_skills/enabled_skills.py +82 -0
- code_puppy_core_plugins/agent_skills/installer.py +22 -0
- code_puppy_core_plugins/agent_skills/metadata.py +219 -0
- code_puppy_core_plugins/agent_skills/prompt_builder.py +49 -0
- code_puppy_core_plugins/agent_skills/provider.py +52 -0
- code_puppy_core_plugins/agent_skills/register_callbacks.py +309 -0
- code_puppy_core_plugins/agent_skills/remote_catalog.py +322 -0
- code_puppy_core_plugins/agent_skills/skill_catalog.py +257 -0
- code_puppy_core_plugins/agent_skills/skill_commands.py +102 -0
- code_puppy_core_plugins/agent_skills/skills_install_menu.py +698 -0
- code_puppy_core_plugins/agent_skills/skills_menu.py +794 -0
- code_puppy_core_plugins/aws_bedrock/__init__.py +14 -0
- code_puppy_core_plugins/aws_bedrock/config.py +101 -0
- code_puppy_core_plugins/aws_bedrock/register_callbacks.py +243 -0
- code_puppy_core_plugins/aws_bedrock/utils.py +186 -0
- code_puppy_core_plugins/azure_foundry/README.md +238 -0
- code_puppy_core_plugins/azure_foundry/__init__.py +15 -0
- code_puppy_core_plugins/azure_foundry/config.py +126 -0
- code_puppy_core_plugins/azure_foundry/discovery.py +189 -0
- code_puppy_core_plugins/azure_foundry/register_callbacks.py +506 -0
- code_puppy_core_plugins/azure_foundry/token.py +182 -0
- code_puppy_core_plugins/azure_foundry/utils.py +375 -0
- code_puppy_core_plugins/btw/__init__.py +1 -0
- code_puppy_core_plugins/btw/inline_view.py +135 -0
- code_puppy_core_plugins/btw/register_callbacks.py +106 -0
- code_puppy_core_plugins/btw/side_query.py +91 -0
- code_puppy_core_plugins/chatgpt_oauth/IMAGEGEN_SKILL.md +83 -0
- code_puppy_core_plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy_core_plugins/chatgpt_oauth/config.py +52 -0
- code_puppy_core_plugins/chatgpt_oauth/image_generation.py +253 -0
- code_puppy_core_plugins/chatgpt_oauth/image_tool.py +70 -0
- code_puppy_core_plugins/chatgpt_oauth/oauth_flow.py +461 -0
- code_puppy_core_plugins/chatgpt_oauth/register_callbacks.py +288 -0
- code_puppy_core_plugins/chatgpt_oauth/test_plugin.py +663 -0
- code_puppy_core_plugins/chatgpt_oauth/usage.py +113 -0
- code_puppy_core_plugins/chatgpt_oauth/utils.py +534 -0
- code_puppy_core_plugins/claude_code_hooks/__init__.py +1 -0
- code_puppy_core_plugins/claude_code_hooks/config.py +137 -0
- code_puppy_core_plugins/claude_code_hooks/register_callbacks.py +383 -0
- code_puppy_core_plugins/claude_code_oauth/README.md +167 -0
- code_puppy_core_plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy_core_plugins/claude_code_oauth/__init__.py +25 -0
- code_puppy_core_plugins/claude_code_oauth/config.py +59 -0
- code_puppy_core_plugins/claude_code_oauth/fast_mode.py +122 -0
- code_puppy_core_plugins/claude_code_oauth/prompt_handler.py +64 -0
- code_puppy_core_plugins/claude_code_oauth/register_callbacks.py +719 -0
- code_puppy_core_plugins/claude_code_oauth/test_plugin.py +285 -0
- code_puppy_core_plugins/claude_code_oauth/token_refresh_heartbeat.py +240 -0
- code_puppy_core_plugins/claude_code_oauth/utils.py +663 -0
- code_puppy_core_plugins/code_puppy_agent/AGENTS_AND_TOOLS.md +155 -0
- code_puppy_core_plugins/code_puppy_agent/MODELS_AND_MCP.md +104 -0
- code_puppy_core_plugins/code_puppy_agent/PLUGINS_AND_CALLBACKS.md +147 -0
- code_puppy_core_plugins/code_puppy_agent/SESSIONS_AND_HISTORY.md +39 -0
- code_puppy_core_plugins/code_puppy_agent/SKILL.md +180 -0
- code_puppy_core_plugins/code_puppy_agent/SKILLS_SYSTEM.md +136 -0
- code_puppy_core_plugins/code_puppy_agent/SYSTEM_PROMPT_CONFIG_AND_I18N.md +160 -0
- code_puppy_core_plugins/code_puppy_agent/__init__.py +5 -0
- code_puppy_core_plugins/code_puppy_agent/register_callbacks.py +46 -0
- code_puppy_core_plugins/computer_use/NativeCapture.swift +123 -0
- code_puppy_core_plugins/computer_use/README.md +109 -0
- code_puppy_core_plugins/computer_use/__init__.py +1 -0
- code_puppy_core_plugins/computer_use/accessibility.py +163 -0
- code_puppy_core_plugins/computer_use/activation.py +75 -0
- code_puppy_core_plugins/computer_use/backend.py +584 -0
- code_puppy_core_plugins/computer_use/backend_types.py +5 -0
- code_puppy_core_plugins/computer_use/capture.py +110 -0
- code_puppy_core_plugins/computer_use/commands.py +62 -0
- code_puppy_core_plugins/computer_use/geometry.py +81 -0
- code_puppy_core_plugins/computer_use/inline_image.py +56 -0
- code_puppy_core_plugins/computer_use/keycodes.py +47 -0
- code_puppy_core_plugins/computer_use/policy.py +144 -0
- code_puppy_core_plugins/computer_use/register_callbacks.py +96 -0
- code_puppy_core_plugins/computer_use/safety.py +18 -0
- code_puppy_core_plugins/computer_use/settle.py +64 -0
- code_puppy_core_plugins/computer_use/state.py +86 -0
- code_puppy_core_plugins/computer_use/tools.py +469 -0
- code_puppy_core_plugins/context_indicator/__init__.py +14 -0
- code_puppy_core_plugins/context_indicator/register_callbacks.py +249 -0
- code_puppy_core_plugins/context_indicator/usage.py +40 -0
- code_puppy_core_plugins/copilot_auth/__init__.py +11 -0
- code_puppy_core_plugins/copilot_auth/config.py +93 -0
- code_puppy_core_plugins/copilot_auth/reasoning_client.py +409 -0
- code_puppy_core_plugins/copilot_auth/register_callbacks.py +460 -0
- code_puppy_core_plugins/copilot_auth/utils.py +587 -0
- code_puppy_core_plugins/customizable_commands/__init__.py +0 -0
- code_puppy_core_plugins/customizable_commands/register_callbacks.py +464 -0
- code_puppy_core_plugins/dbos_durable_exec/__init__.py +1 -0
- code_puppy_core_plugins/dbos_durable_exec/cancel.py +15 -0
- code_puppy_core_plugins/dbos_durable_exec/commands.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/config.py +31 -0
- code_puppy_core_plugins/dbos_durable_exec/lifecycle.py +83 -0
- code_puppy_core_plugins/dbos_durable_exec/register_callbacks.py +45 -0
- code_puppy_core_plugins/dbos_durable_exec/runtime.py +65 -0
- code_puppy_core_plugins/dbos_durable_exec/startup_lock.py +155 -0
- code_puppy_core_plugins/dbos_durable_exec/workflow_ids.py +16 -0
- code_puppy_core_plugins/dbos_durable_exec/wrapper.py +58 -0
- code_puppy_core_plugins/destructive_command_guard/AGENTS.md +79 -0
- code_puppy_core_plugins/destructive_command_guard/__init__.py +14 -0
- code_puppy_core_plugins/destructive_command_guard/detector.py +374 -0
- code_puppy_core_plugins/destructive_command_guard/register_callbacks.py +165 -0
- code_puppy_core_plugins/emoji_filter/__init__.py +12 -0
- code_puppy_core_plugins/emoji_filter/config.py +26 -0
- code_puppy_core_plugins/emoji_filter/register_callbacks.py +238 -0
- code_puppy_core_plugins/emoji_filter/stripper.py +45 -0
- code_puppy_core_plugins/empty_html_comment_filter/register_callbacks.py +116 -0
- code_puppy_core_plugins/example_custom_command/README.md +328 -0
- code_puppy_core_plugins/example_custom_command/register_callbacks.py +66 -0
- code_puppy_core_plugins/file_permission_handler/__init__.py +4 -0
- code_puppy_core_plugins/file_permission_handler/register_callbacks.py +575 -0
- code_puppy_core_plugins/flux_bootstrap/__init__.py +5 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/README.md +173 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/cheatsheet.md +86 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/pipeline.md +167 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/_docs/synopsis.md +392 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/about.md +51 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/address-feedback.md +129 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/ask.md +211 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/aug.md +181 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/auto-pilot.md +71 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/cheatsheet.md +16 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/commit.md +103 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/config.md +108 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/create-pr.md +195 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/exec.md +221 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/new.md +151 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/qa.md +208 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/rebase.md +180 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/review.md +304 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/split.md +120 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/squash-commits.md +179 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/status.md +46 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/commands/flux/tests.md +155 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_about.py +154 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_cheatsheet.py +248 -0
- code_puppy_core_plugins/flux_bootstrap/bundled/scripts/flux_status.py +345 -0
- code_puppy_core_plugins/flux_bootstrap/installer.py +256 -0
- code_puppy_core_plugins/flux_bootstrap/register_callbacks.py +71 -0
- code_puppy_core_plugins/force_push_guard/__init__.py +5 -0
- code_puppy_core_plugins/force_push_guard/detector.py +96 -0
- code_puppy_core_plugins/force_push_guard/register_callbacks.py +161 -0
- code_puppy_core_plugins/fork/__init__.py +7 -0
- code_puppy_core_plugins/fork/register_callbacks.py +445 -0
- code_puppy_core_plugins/frontend_emitter/__init__.py +25 -0
- code_puppy_core_plugins/frontend_emitter/emitter.py +249 -0
- code_puppy_core_plugins/frontend_emitter/register_callbacks.py +412 -0
- code_puppy_core_plugins/frontend_emitter/session_context.py +50 -0
- code_puppy_core_plugins/grok_oauth/__init__.py +6 -0
- code_puppy_core_plugins/grok_oauth/config.py +49 -0
- code_puppy_core_plugins/grok_oauth/oauth_flow.py +213 -0
- code_puppy_core_plugins/grok_oauth/register_callbacks.py +132 -0
- code_puppy_core_plugins/grok_oauth/utils.py +195 -0
- code_puppy_core_plugins/herdr/README.md +168 -0
- code_puppy_core_plugins/herdr/__init__.py +9 -0
- code_puppy_core_plugins/herdr/client.py +339 -0
- code_puppy_core_plugins/herdr/register_callbacks.py +140 -0
- code_puppy_core_plugins/herdr/reporter.py +212 -0
- code_puppy_core_plugins/herdr/smoke.py +143 -0
- code_puppy_core_plugins/herdr/sources.py +133 -0
- code_puppy_core_plugins/hook_creator/__init__.py +1 -0
- code_puppy_core_plugins/hook_creator/register_callbacks.py +33 -0
- code_puppy_core_plugins/hook_manager/__init__.py +1 -0
- code_puppy_core_plugins/hook_manager/config.py +290 -0
- code_puppy_core_plugins/hook_manager/hooks_menu.py +571 -0
- code_puppy_core_plugins/hook_manager/register_callbacks.py +227 -0
- code_puppy_core_plugins/meta_oauth/__init__.py +1 -0
- code_puppy_core_plugins/meta_oauth/config.py +55 -0
- code_puppy_core_plugins/meta_oauth/oauth_flow.py +109 -0
- code_puppy_core_plugins/meta_oauth/register_callbacks.py +154 -0
- code_puppy_core_plugins/meta_oauth/utils.py +252 -0
- code_puppy_core_plugins/namespace_skill_search/README.md +118 -0
- code_puppy_core_plugins/namespace_skill_search/__init__.py +15 -0
- code_puppy_core_plugins/namespace_skill_search/namespaces.py +125 -0
- code_puppy_core_plugins/namespace_skill_search/register_callbacks.py +99 -0
- code_puppy_core_plugins/namespace_skill_search/search_tool.py +158 -0
- code_puppy_core_plugins/no_tools/__init__.py +0 -0
- code_puppy_core_plugins/no_tools/register_callbacks.py +42 -0
- code_puppy_core_plugins/oauth_pasteback.py +110 -0
- code_puppy_core_plugins/oauth_puppy_html.py +194 -0
- code_puppy_core_plugins/obsidian_agent/README.md +52 -0
- code_puppy_core_plugins/obsidian_agent/__init__.py +8 -0
- code_puppy_core_plugins/obsidian_agent/agent_obsidian.py +147 -0
- code_puppy_core_plugins/obsidian_agent/register_callbacks.py +13 -0
- code_puppy_core_plugins/ollama/__init__.py +1 -0
- code_puppy_core_plugins/ollama/register_callbacks.py +127 -0
- code_puppy_core_plugins/ollama_setup/__init__.py +5 -0
- code_puppy_core_plugins/ollama_setup/completer.py +38 -0
- code_puppy_core_plugins/ollama_setup/register_callbacks.py +419 -0
- code_puppy_core_plugins/plugin_list/__init__.py +0 -0
- code_puppy_core_plugins/plugin_list/plugin_contributions.py +348 -0
- code_puppy_core_plugins/plugin_list/plugin_meta.py +67 -0
- code_puppy_core_plugins/plugin_list/plugin_text_utils.py +173 -0
- code_puppy_core_plugins/plugin_list/plugins_menu.py +455 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_layout.py +194 -0
- code_puppy_core_plugins/plugin_list/plugins_menu_render.py +431 -0
- code_puppy_core_plugins/plugin_list/project_trust_flow.py +162 -0
- code_puppy_core_plugins/plugin_list/register_callbacks.py +281 -0
- code_puppy_core_plugins/pop_command/__init__.py +1 -0
- code_puppy_core_plugins/pop_command/register_callbacks.py +191 -0
- code_puppy_core_plugins/prompt_newline/__init__.py +13 -0
- code_puppy_core_plugins/prompt_newline/config.py +21 -0
- code_puppy_core_plugins/prompt_newline/register_callbacks.py +163 -0
- code_puppy_core_plugins/prune/__init__.py +1 -0
- code_puppy_core_plugins/prune/prune_menu.py +308 -0
- code_puppy_core_plugins/prune/prune_model.py +517 -0
- code_puppy_core_plugins/prune/prune_render.py +354 -0
- code_puppy_core_plugins/prune/register_callbacks.py +388 -0
- code_puppy_core_plugins/puppy_kennel/README.md +203 -0
- code_puppy_core_plugins/puppy_kennel/__init__.py +6 -0
- code_puppy_core_plugins/puppy_kennel/commands.py +206 -0
- code_puppy_core_plugins/puppy_kennel/config.py +36 -0
- code_puppy_core_plugins/puppy_kennel/kennel.py +368 -0
- code_puppy_core_plugins/puppy_kennel/packer.py +152 -0
- code_puppy_core_plugins/puppy_kennel/recorder.py +83 -0
- code_puppy_core_plugins/puppy_kennel/register_callbacks.py +109 -0
- code_puppy_core_plugins/puppy_kennel/retriever.py +28 -0
- code_puppy_core_plugins/puppy_kennel/schema.py +78 -0
- code_puppy_core_plugins/puppy_kennel/state.py +68 -0
- code_puppy_core_plugins/puppy_kennel/tools.py +449 -0
- code_puppy_core_plugins/puppy_kennel/wings.py +110 -0
- code_puppy_core_plugins/puppy_spinner/README.md +93 -0
- code_puppy_core_plugins/puppy_spinner/__init__.py +11 -0
- code_puppy_core_plugins/puppy_spinner/builtin_frames.py +172 -0
- code_puppy_core_plugins/puppy_spinner/commands.py +116 -0
- code_puppy_core_plugins/puppy_spinner/picker.py +379 -0
- code_puppy_core_plugins/puppy_spinner/register_callbacks.py +193 -0
- code_puppy_core_plugins/puppy_spinner/spinners.py +444 -0
- code_puppy_core_plugins/quick_resume/__init__.py +0 -0
- code_puppy_core_plugins/quick_resume/register_callbacks.py +77 -0
- code_puppy_core_plugins/review_pr/__init__.py +1 -0
- code_puppy_core_plugins/review_pr/register_callbacks.py +162 -0
- code_puppy_core_plugins/shell_safety/__init__.py +6 -0
- code_puppy_core_plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy_core_plugins/shell_safety/command_cache.py +156 -0
- code_puppy_core_plugins/shell_safety/register_callbacks.py +203 -0
- code_puppy_core_plugins/stack_dump/__init__.py +1 -0
- code_puppy_core_plugins/stack_dump/register_callbacks.py +79 -0
- code_puppy_core_plugins/statusline/__init__.py +14 -0
- code_puppy_core_plugins/statusline/config.py +68 -0
- code_puppy_core_plugins/statusline/payload.py +138 -0
- code_puppy_core_plugins/statusline/prompt_patch.py +76 -0
- code_puppy_core_plugins/statusline/register_callbacks.py +45 -0
- code_puppy_core_plugins/statusline/runner.py +122 -0
- code_puppy_core_plugins/statusline/statusline_command.py +197 -0
- code_puppy_core_plugins/steer_queue/__init__.py +10 -0
- code_puppy_core_plugins/steer_queue/queue_menu.py +444 -0
- code_puppy_core_plugins/steer_queue/register_callbacks.py +138 -0
- code_puppy_core_plugins/subagent_panel/README.md +102 -0
- code_puppy_core_plugins/subagent_panel/__init__.py +24 -0
- code_puppy_core_plugins/subagent_panel/coalesce_patch.py +242 -0
- code_puppy_core_plugins/subagent_panel/panel_render.py +145 -0
- code_puppy_core_plugins/subagent_panel/register_callbacks.py +535 -0
- code_puppy_core_plugins/subagent_panel/resume_repaint.py +112 -0
- code_puppy_core_plugins/subagent_panel/state.py +231 -0
- code_puppy_core_plugins/switch_agent_resume/__init__.py +5 -0
- code_puppy_core_plugins/switch_agent_resume/register_callbacks.py +314 -0
- code_puppy_core_plugins/theme/README.md +86 -0
- code_puppy_core_plugins/theme/__init__.py +14 -0
- code_puppy_core_plugins/theme/bundled_palettes.py +346 -0
- code_puppy_core_plugins/theme/content_styles.py +146 -0
- code_puppy_core_plugins/theme/osc_palette.py +178 -0
- code_puppy_core_plugins/theme/picker.py +358 -0
- code_puppy_core_plugins/theme/prompt_toolkit_theme.py +113 -0
- code_puppy_core_plugins/theme/register_callbacks.py +348 -0
- code_puppy_core_plugins/theme/rich_themes.py +202 -0
- code_puppy_core_plugins/theme/themes.py +720 -0
- code_puppy_core_plugins/timestamp_heartbeat/__init__.py +1 -0
- code_puppy_core_plugins/timestamp_heartbeat/register_callbacks.py +114 -0
- code_puppy_core_plugins/token_ratio_learner/__init__.py +5 -0
- code_puppy_core_plugins/token_ratio_learner/ratios.py +208 -0
- code_puppy_core_plugins/token_ratio_learner/register_callbacks.py +213 -0
- code_puppy_core_plugins/universal_constructor/__init__.py +13 -0
- code_puppy_core_plugins/universal_constructor/models.py +138 -0
- code_puppy_core_plugins/universal_constructor/provider.py +62 -0
- code_puppy_core_plugins/universal_constructor/register_callbacks.py +75 -0
- code_puppy_core_plugins/universal_constructor/registry.py +302 -0
- code_puppy_core_plugins/universal_constructor/sandbox.py +584 -0
- code_puppy_core_plugins/wide_completion_menu/__init__.py +6 -0
- code_puppy_core_plugins/wide_completion_menu/register_callbacks.py +165 -0
- code_puppy_core_plugins/wiggum/__init__.py +1 -0
- code_puppy_core_plugins/wiggum/goal_runs.py +91 -0
- code_puppy_core_plugins/wiggum/judge.py +299 -0
- code_puppy_core_plugins/wiggum/judge_config.py +249 -0
- code_puppy_core_plugins/wiggum/judges_menu.py +814 -0
- code_puppy_core_plugins/wiggum/register_callbacks.py +569 -0
- code_puppy_core_plugins/wiggum/state.py +89 -0
- code_puppy_core_plugins/yolo_cli/__init__.py +1 -0
- code_puppy_core_plugins/yolo_cli/register_callbacks.py +41 -0
- code_puppy_core_plugins-0.0.2.dist-info/METADATA +64 -0
- code_puppy_core_plugins-0.0.2.dist-info/RECORD +313 -0
- code_puppy_core_plugins-0.0.2.dist-info/WHEEL +4 -0
- code_puppy_core_plugins-0.0.2.dist-info/entry_points.txt +55 -0
- code_puppy_core_plugins-0.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Session-id ContextVar used by the frontend_emitter plugin.
|
|
2
|
+
|
|
3
|
+
This module defines a single ``contextvars.ContextVar`` that carries
|
|
4
|
+
the *current agent invocation's* session identifier across the asyncio
|
|
5
|
+
call graph without requiring every callable to take an explicit kwarg.
|
|
6
|
+
|
|
7
|
+
The primary use case is letting embedders (e.g. a WebSocket backend
|
|
8
|
+
hosting multiple concurrent agent sessions) tag every event emitted
|
|
9
|
+
during an agent run with a session identifier, so subscribers can
|
|
10
|
+
filter events by session without monkey-patching the emitter.
|
|
11
|
+
|
|
12
|
+
Usage::
|
|
13
|
+
|
|
14
|
+
from code_puppy_core_plugins.frontend_emitter.session_context import (
|
|
15
|
+
current_emitter_session_id,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
token = current_emitter_session_id.set("my-session-id")
|
|
19
|
+
try:
|
|
20
|
+
await some_agent_work() # any emit_event() inside picks up the sid
|
|
21
|
+
finally:
|
|
22
|
+
current_emitter_session_id.reset(token)
|
|
23
|
+
|
|
24
|
+
Because ``ContextVar`` values are copied into each new asyncio Task at
|
|
25
|
+
creation time, sessions are naturally isolated across concurrent tasks
|
|
26
|
+
without explicit propagation.
|
|
27
|
+
|
|
28
|
+
Scope note
|
|
29
|
+
----------
|
|
30
|
+
This ContextVar is intentionally scoped to the ``frontend_emitter``
|
|
31
|
+
plugin. It is not a general-purpose ``code_puppy.context.session_id``
|
|
32
|
+
because the only consumer is the emitter's session-channel fan-out
|
|
33
|
+
logic. Other subsystems that need session-level metadata should
|
|
34
|
+
introduce their own ContextVar with a clearly-scoped name.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from contextvars import ContextVar
|
|
40
|
+
from typing import Optional
|
|
41
|
+
|
|
42
|
+
# session_id of the currently running agent invocation, if any. Set by embedders
|
|
43
|
+
# before agent work; read by emit_event()/register_callbacks to tag events. ``None``
|
|
44
|
+
# is the legitimate "no session context" value, not an error.
|
|
45
|
+
current_emitter_session_id: ContextVar[Optional[str]] = ContextVar(
|
|
46
|
+
"code_puppy_frontend_emitter_session_id", default=None
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ["current_emitter_session_id"]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Configuration for the Grok (x.ai) OAuth plugin.
|
|
2
|
+
|
|
3
|
+
Endpoints, client id, and scopes mirror the official Grok CLI OAuth flow
|
|
4
|
+
(as implemented by https://pi.dev/packages/pi-xai-oauth).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
|
|
10
|
+
from code_puppy import config
|
|
11
|
+
|
|
12
|
+
GROK_OAUTH_CONFIG: Dict[str, Any] = {
|
|
13
|
+
# OIDC issuer + discovery. Authorization/token endpoints are resolved
|
|
14
|
+
# dynamically from the discovery document and validated against *.x.ai.
|
|
15
|
+
"issuer": "https://auth.x.ai",
|
|
16
|
+
"discovery_url": "https://auth.x.ai/.well-known/openid-configuration",
|
|
17
|
+
# Public OAuth client used by the official Grok CLI.
|
|
18
|
+
"client_id": "b1a00492-073a-47ea-816f-4c329264a828",
|
|
19
|
+
"scope": "openid profile email offline_access grok-cli:access api:access",
|
|
20
|
+
# Localhost callback. xAI allows an ephemeral-port fallback when the
|
|
21
|
+
# preferred port is busy.
|
|
22
|
+
"redirect_host": "127.0.0.1",
|
|
23
|
+
"redirect_port": 56121,
|
|
24
|
+
"redirect_path": "/callback",
|
|
25
|
+
"callback_timeout": 180,
|
|
26
|
+
# Refresh this many seconds before the token actually expires.
|
|
27
|
+
"refresh_skew_seconds": 120,
|
|
28
|
+
# Grok models are served from the regular xAI API with the OAuth
|
|
29
|
+
# access token as the bearer key.
|
|
30
|
+
"api_base_url": "https://api.x.ai/v1",
|
|
31
|
+
# Registered model names are prefixed to avoid catalogue collisions.
|
|
32
|
+
"prefix": "xai-",
|
|
33
|
+
"default_model": "grok-4.5",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Grok models made available after authentication.
|
|
37
|
+
GROK_MODELS: Dict[str, Dict[str, Any]] = {
|
|
38
|
+
"grok-4.5": {
|
|
39
|
+
"context_length": 500_000,
|
|
40
|
+
"supported_settings": ["reasoning_effort"],
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_token_storage_path() -> Path:
|
|
46
|
+
"""Path for storing OAuth tokens (uses XDG_DATA_HOME)."""
|
|
47
|
+
data_dir = Path(config.DATA_DIR)
|
|
48
|
+
data_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
49
|
+
return data_dir / "grok_oauth.json"
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Grok (x.ai) OAuth flow matching the official Grok CLI / pi-xai-oauth."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import secrets
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import urllib.parse
|
|
9
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
|
|
13
|
+
|
|
14
|
+
from ..oauth_puppy_html import oauth_failure_html, oauth_success_html
|
|
15
|
+
from .config import GROK_OAUTH_CONFIG
|
|
16
|
+
from .utils import (
|
|
17
|
+
exchange_token,
|
|
18
|
+
fetch_discovery,
|
|
19
|
+
generate_pkce_pair,
|
|
20
|
+
load_stored_tokens,
|
|
21
|
+
save_tokens,
|
|
22
|
+
tokens_from_payload,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_ALLOWED_CORS_ORIGINS = ("https://accounts.x.ai", "https://auth.x.ai")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _OAuthServer(HTTPServer):
|
|
29
|
+
"""Localhost callback server holding the state for one OAuth attempt."""
|
|
30
|
+
|
|
31
|
+
def __init__(self) -> None:
|
|
32
|
+
host = GROK_OAUTH_CONFIG["redirect_host"]
|
|
33
|
+
preferred_port = GROK_OAUTH_CONFIG["redirect_port"]
|
|
34
|
+
try:
|
|
35
|
+
super().__init__((host, preferred_port), _CallbackHandler)
|
|
36
|
+
except OSError:
|
|
37
|
+
# Preferred port busy — xAI accepts ephemeral localhost ports.
|
|
38
|
+
super().__init__((host, 0), _CallbackHandler)
|
|
39
|
+
|
|
40
|
+
self.exit_code = 1
|
|
41
|
+
discovery = fetch_discovery()
|
|
42
|
+
self.authorization_endpoint = discovery["authorization_endpoint"]
|
|
43
|
+
self.token_endpoint = discovery["token_endpoint"]
|
|
44
|
+
self.state = secrets.token_hex(16)
|
|
45
|
+
self.nonce = secrets.token_hex(16)
|
|
46
|
+
self.code_verifier, self.code_challenge = generate_pkce_pair()
|
|
47
|
+
port = self.server_address[1]
|
|
48
|
+
self.redirect_uri = f"http://{host}:{port}{GROK_OAUTH_CONFIG['redirect_path']}"
|
|
49
|
+
|
|
50
|
+
def auth_url(self) -> str:
|
|
51
|
+
params = {
|
|
52
|
+
"response_type": "code",
|
|
53
|
+
"client_id": GROK_OAUTH_CONFIG["client_id"],
|
|
54
|
+
"redirect_uri": self.redirect_uri,
|
|
55
|
+
"scope": GROK_OAUTH_CONFIG["scope"],
|
|
56
|
+
"code_challenge": self.code_challenge,
|
|
57
|
+
"code_challenge_method": "S256",
|
|
58
|
+
"state": self.state,
|
|
59
|
+
"nonce": self.nonce,
|
|
60
|
+
}
|
|
61
|
+
return f"{self.authorization_endpoint}?{urllib.parse.urlencode(params)}"
|
|
62
|
+
|
|
63
|
+
def exchange_code(self, code: str) -> bool:
|
|
64
|
+
payload = exchange_token(
|
|
65
|
+
self.token_endpoint,
|
|
66
|
+
{
|
|
67
|
+
"grant_type": "authorization_code",
|
|
68
|
+
"code": code,
|
|
69
|
+
"redirect_uri": self.redirect_uri,
|
|
70
|
+
"client_id": GROK_OAUTH_CONFIG["client_id"],
|
|
71
|
+
"code_verifier": self.code_verifier,
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
return save_tokens(tokens_from_payload(payload, self.token_endpoint))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
78
|
+
server: "_OAuthServer"
|
|
79
|
+
|
|
80
|
+
def do_OPTIONS(self) -> None: # noqa: N802
|
|
81
|
+
self.send_response(204)
|
|
82
|
+
self._send_cors_headers()
|
|
83
|
+
self.end_headers()
|
|
84
|
+
|
|
85
|
+
def do_GET(self) -> None: # noqa: N802
|
|
86
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
87
|
+
if parsed.path != GROK_OAUTH_CONFIG["redirect_path"]:
|
|
88
|
+
self._send_failure(404, "Callback endpoint not found for the puppy parade.")
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
params = urllib.parse.parse_qs(parsed.query)
|
|
92
|
+
error = params.get("error", [None])[0]
|
|
93
|
+
if error:
|
|
94
|
+
description = params.get("error_description", [error])[0]
|
|
95
|
+
self._send_failure(400, f"xAI authorization failed: {description}")
|
|
96
|
+
self._shutdown_after_delay()
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if params.get("state", [None])[0] != self.server.state:
|
|
100
|
+
self._send_failure(
|
|
101
|
+
400, "OAuth state mismatch — a squirrel may have tampered with it."
|
|
102
|
+
)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
code = params.get("code", [None])[0]
|
|
106
|
+
if not code:
|
|
107
|
+
self._send_failure(400, "Missing auth code — the token treat rolled away.")
|
|
108
|
+
self._shutdown_after_delay()
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
saved = self.server.exchange_code(code)
|
|
113
|
+
except Exception as exc: # noqa: BLE001
|
|
114
|
+
self._send_failure(500, f"Token exchange failed: {exc}")
|
|
115
|
+
self._shutdown_after_delay()
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
if saved:
|
|
119
|
+
self.server.exit_code = 0
|
|
120
|
+
self._send_html(
|
|
121
|
+
oauth_success_html(
|
|
122
|
+
"Grok",
|
|
123
|
+
"You can now close this window and return to Code Puppy.",
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
self._send_failure(
|
|
128
|
+
500, "Unable to persist auth file — a puppy probably chewed it."
|
|
129
|
+
)
|
|
130
|
+
self._shutdown_after_delay()
|
|
131
|
+
|
|
132
|
+
def log_message(self, fmt: str, *args: Any) -> None: # noqa: A003
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
def _send_cors_headers(self) -> None:
|
|
136
|
+
origin = self.headers.get("Origin")
|
|
137
|
+
if origin not in _ALLOWED_CORS_ORIGINS:
|
|
138
|
+
return
|
|
139
|
+
self.send_header("Access-Control-Allow-Origin", origin)
|
|
140
|
+
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
|
141
|
+
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
|
142
|
+
self.send_header("Access-Control-Allow-Private-Network", "true")
|
|
143
|
+
self.send_header("Vary", "Origin")
|
|
144
|
+
|
|
145
|
+
def _send_html(self, body: str, status: int = 200) -> None:
|
|
146
|
+
encoded = body.encode("utf-8")
|
|
147
|
+
self.send_response(status)
|
|
148
|
+
self._send_cors_headers()
|
|
149
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
150
|
+
self.send_header("Content-Length", str(len(encoded)))
|
|
151
|
+
self.end_headers()
|
|
152
|
+
self.wfile.write(encoded)
|
|
153
|
+
|
|
154
|
+
def _send_failure(self, status: int, reason: str) -> None:
|
|
155
|
+
self._send_html(oauth_failure_html("Grok", reason), status)
|
|
156
|
+
|
|
157
|
+
def _shutdown_after_delay(self, seconds: float = 2.0) -> None:
|
|
158
|
+
def _later() -> None:
|
|
159
|
+
time.sleep(seconds)
|
|
160
|
+
self.server.shutdown()
|
|
161
|
+
|
|
162
|
+
threading.Thread(target=_later, daemon=True).start()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def run_oauth_flow() -> bool:
|
|
166
|
+
"""Run the browser OAuth flow. Returns True when tokens were saved."""
|
|
167
|
+
existing_tokens = load_stored_tokens()
|
|
168
|
+
if existing_tokens and existing_tokens.get("access_token"):
|
|
169
|
+
emit_warning("Existing Grok tokens will be overwritten.")
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
server = _OAuthServer()
|
|
173
|
+
except Exception as exc: # noqa: BLE001
|
|
174
|
+
emit_error(f"Could not start Grok OAuth flow: {exc}")
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
auth_url = server.auth_url()
|
|
178
|
+
emit_info(f"Open this URL in your browser: {auth_url}")
|
|
179
|
+
|
|
180
|
+
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
181
|
+
server_thread.start()
|
|
182
|
+
|
|
183
|
+
from code_puppy.tools.common import should_suppress_browser
|
|
184
|
+
|
|
185
|
+
if should_suppress_browser():
|
|
186
|
+
emit_info(f"[HEADLESS MODE] Would normally open: {auth_url}")
|
|
187
|
+
else:
|
|
188
|
+
try:
|
|
189
|
+
import webbrowser
|
|
190
|
+
|
|
191
|
+
if not webbrowser.open(auth_url):
|
|
192
|
+
emit_warning("Please open the URL manually — the browser stayed home.")
|
|
193
|
+
except Exception as exc: # noqa: BLE001
|
|
194
|
+
emit_warning(f"Could not open browser automatically: {exc}")
|
|
195
|
+
|
|
196
|
+
emit_info("Waiting for xAI authentication callback…")
|
|
197
|
+
|
|
198
|
+
elapsed = 0.0
|
|
199
|
+
timeout = GROK_OAUTH_CONFIG["callback_timeout"]
|
|
200
|
+
interval = 0.25
|
|
201
|
+
while elapsed < timeout and server.exit_code != 0:
|
|
202
|
+
time.sleep(interval)
|
|
203
|
+
elapsed += interval
|
|
204
|
+
|
|
205
|
+
server.shutdown()
|
|
206
|
+
server_thread.join(timeout=5)
|
|
207
|
+
|
|
208
|
+
if server.exit_code != 0:
|
|
209
|
+
emit_error("Grok authentication failed or timed out.")
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
emit_success("Grok OAuth complete — access token saved.")
|
|
213
|
+
return True
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Grok (x.ai) OAuth plugin callbacks.
|
|
2
|
+
|
|
3
|
+
Provides OAuth authentication for Grok models (SuperGrok / Grok CLI flow)
|
|
4
|
+
and registers the 'grok_oauth' model type handler plus the Grok model
|
|
5
|
+
catalogue (headlined by grok-4.5).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from code_puppy.callbacks import register_callback
|
|
13
|
+
from code_puppy.messaging import emit_info, emit_success, emit_warning
|
|
14
|
+
from code_puppy.model_switching import set_model_and_reload_agent
|
|
15
|
+
|
|
16
|
+
from .config import GROK_MODELS, GROK_OAUTH_CONFIG, get_token_storage_path
|
|
17
|
+
from .oauth_flow import run_oauth_flow
|
|
18
|
+
from .utils import get_valid_access_token, load_stored_tokens
|
|
19
|
+
|
|
20
|
+
_PREFIX = GROK_OAUTH_CONFIG["prefix"]
|
|
21
|
+
_DEFAULT_MODEL = f"{_PREFIX}{GROK_OAUTH_CONFIG['default_model']}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _custom_help() -> List[Tuple[str, str]]:
|
|
25
|
+
return [
|
|
26
|
+
(
|
|
27
|
+
"grok-auth",
|
|
28
|
+
"Authenticate with x.ai (Grok) via OAuth and register Grok models",
|
|
29
|
+
),
|
|
30
|
+
("grok-status", "Check Grok OAuth authentication status"),
|
|
31
|
+
("grok-logout", "Remove Grok OAuth tokens and registered models"),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _handle_grok_status() -> None:
|
|
36
|
+
tokens = load_stored_tokens()
|
|
37
|
+
if tokens and tokens.get("access_token"):
|
|
38
|
+
emit_success(" Grok OAuth: Authenticated")
|
|
39
|
+
emit_info(f" Available Grok models: {', '.join(_grok_model_names())}")
|
|
40
|
+
else:
|
|
41
|
+
emit_warning(" Grok OAuth: Not authenticated")
|
|
42
|
+
emit_info(" Run /grok-auth to launch the browser sign-in flow.")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _handle_grok_logout() -> None:
|
|
46
|
+
token_path = get_token_storage_path()
|
|
47
|
+
if token_path.exists():
|
|
48
|
+
token_path.unlink()
|
|
49
|
+
emit_info("Removed Grok OAuth tokens")
|
|
50
|
+
emit_success("Grok logout complete")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _handle_custom_command(command: str, name: str) -> Optional[bool]:
|
|
54
|
+
if not name:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
if name == "grok-auth":
|
|
58
|
+
if run_oauth_flow():
|
|
59
|
+
emit_info(f"Registering Grok models: {', '.join(_grok_model_names())}")
|
|
60
|
+
set_model_and_reload_agent(_DEFAULT_MODEL)
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
if name == "grok-status":
|
|
64
|
+
_handle_grok_status()
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
if name == "grok-logout":
|
|
68
|
+
_handle_grok_logout()
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _grok_model_names() -> List[str]:
|
|
75
|
+
return [f"{_PREFIX}{model_id}" for model_id in GROK_MODELS]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _load_grok_models() -> Dict[str, Any]:
|
|
79
|
+
"""Inject Grok models into the catalogue once authenticated."""
|
|
80
|
+
tokens = load_stored_tokens()
|
|
81
|
+
if not (tokens and tokens.get("access_token")):
|
|
82
|
+
return {}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
f"{_PREFIX}{model_id}": {
|
|
86
|
+
"type": "grok_oauth",
|
|
87
|
+
"name": model_id,
|
|
88
|
+
"context_length": spec["context_length"],
|
|
89
|
+
"supported_settings": spec.get("supported_settings", []),
|
|
90
|
+
"oauth_source": "grok-oauth-plugin",
|
|
91
|
+
}
|
|
92
|
+
for model_id, spec in GROK_MODELS.items()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _create_grok_oauth_model(model_name: str, model_config: Dict, config: Dict) -> Any:
|
|
97
|
+
"""Create a Grok OAuth model instance.
|
|
98
|
+
|
|
99
|
+
This handler is registered via the 'register_model_type' callback to
|
|
100
|
+
handle models with type='grok_oauth'. Grok models speak the OpenAI
|
|
101
|
+
Responses API on api.x.ai with the OAuth access token as bearer key.
|
|
102
|
+
"""
|
|
103
|
+
from pydantic_ai.models.openai import OpenAIResponsesModel
|
|
104
|
+
from pydantic_ai.providers.openai import OpenAIProvider
|
|
105
|
+
|
|
106
|
+
from code_puppy.http_utils import create_async_client
|
|
107
|
+
|
|
108
|
+
access_token = get_valid_access_token()
|
|
109
|
+
if not access_token:
|
|
110
|
+
emit_warning(
|
|
111
|
+
f"Failed to get valid Grok OAuth token; skipping model "
|
|
112
|
+
f"'{model_config.get('name')}'. Run /grok-auth to authenticate."
|
|
113
|
+
)
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
provider = OpenAIProvider(
|
|
117
|
+
api_key=access_token,
|
|
118
|
+
base_url=GROK_OAUTH_CONFIG["api_base_url"],
|
|
119
|
+
http_client=create_async_client(model_name=model_name),
|
|
120
|
+
)
|
|
121
|
+
return OpenAIResponsesModel(model_name=model_config["name"], provider=provider)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _register_model_types() -> List[Dict[str, Any]]:
|
|
125
|
+
"""Register the grok_oauth model type handler."""
|
|
126
|
+
return [{"type": "grok_oauth", "handler": _create_grok_oauth_model}]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
register_callback("custom_command_help", _custom_help)
|
|
130
|
+
register_callback("custom_command", _handle_custom_command)
|
|
131
|
+
register_callback("register_model_type", _register_model_types)
|
|
132
|
+
register_callback("load_models_config", _load_grok_models)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Utility helpers for the Grok (x.ai) OAuth plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import datetime
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import secrets
|
|
11
|
+
import time
|
|
12
|
+
from typing import Any, Dict, Optional, Tuple
|
|
13
|
+
from urllib.parse import urlparse
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
from .config import GROK_OAUTH_CONFIG, get_token_storage_path
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
_discovery_cache: Optional[Dict[str, str]] = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _urlsafe_b64encode(data: bytes) -> str:
|
|
25
|
+
return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def generate_pkce_pair() -> Tuple[str, str]:
|
|
29
|
+
"""Return a (code_verifier, code_challenge) PKCE S256 pair."""
|
|
30
|
+
code_verifier = _urlsafe_b64encode(secrets.token_bytes(32))
|
|
31
|
+
digest = hashlib.sha256(code_verifier.encode("utf-8")).digest()
|
|
32
|
+
return code_verifier, _urlsafe_b64encode(digest)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def validate_xai_endpoint(url: str) -> str:
|
|
36
|
+
"""Ensure a discovered endpoint is HTTPS on x.ai (or a subdomain)."""
|
|
37
|
+
parsed = urlparse(url)
|
|
38
|
+
host = (parsed.hostname or "").lower()
|
|
39
|
+
if parsed.scheme != "https" or (host != "x.ai" and not host.endswith(".x.ai")):
|
|
40
|
+
raise ValueError(f"xAI OAuth discovery returned an unexpected endpoint: {url}")
|
|
41
|
+
return url
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def fetch_discovery() -> Dict[str, str]:
|
|
45
|
+
"""Fetch (and cache) the xAI OIDC discovery document."""
|
|
46
|
+
global _discovery_cache
|
|
47
|
+
if _discovery_cache:
|
|
48
|
+
return _discovery_cache
|
|
49
|
+
|
|
50
|
+
response = requests.get(
|
|
51
|
+
GROK_OAUTH_CONFIG["discovery_url"],
|
|
52
|
+
headers={"Accept": "application/json"},
|
|
53
|
+
timeout=30,
|
|
54
|
+
)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
data = response.json()
|
|
57
|
+
|
|
58
|
+
authorization_endpoint = data.get("authorization_endpoint")
|
|
59
|
+
token_endpoint = data.get("token_endpoint")
|
|
60
|
+
if not authorization_endpoint or not token_endpoint:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"xAI OAuth discovery response did not include authorization/token endpoints"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
_discovery_cache = {
|
|
66
|
+
"authorization_endpoint": validate_xai_endpoint(authorization_endpoint),
|
|
67
|
+
"token_endpoint": validate_xai_endpoint(token_endpoint),
|
|
68
|
+
}
|
|
69
|
+
return _discovery_cache
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _utc_now_iso() -> str:
|
|
73
|
+
return (
|
|
74
|
+
datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def tokens_from_payload(
|
|
79
|
+
payload: Dict[str, Any],
|
|
80
|
+
token_endpoint: str,
|
|
81
|
+
fallback_refresh: str = "",
|
|
82
|
+
) -> Dict[str, Any]:
|
|
83
|
+
"""Normalize an OAuth token response into our stored-token shape."""
|
|
84
|
+
access_token = payload.get("access_token")
|
|
85
|
+
if not access_token:
|
|
86
|
+
raise ValueError("xAI token response did not include an access token")
|
|
87
|
+
|
|
88
|
+
expires_in = payload.get("expires_in") or 3600
|
|
89
|
+
return {
|
|
90
|
+
"access_token": access_token,
|
|
91
|
+
"refresh_token": payload.get("refresh_token") or fallback_refresh,
|
|
92
|
+
"id_token": payload.get("id_token", ""),
|
|
93
|
+
"token_type": payload.get("token_type", "Bearer"),
|
|
94
|
+
"expires_at": time.time() + float(expires_in),
|
|
95
|
+
"token_endpoint": token_endpoint,
|
|
96
|
+
"last_refresh": _utc_now_iso(),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_stored_tokens() -> Optional[Dict[str, Any]]:
|
|
101
|
+
try:
|
|
102
|
+
token_path = get_token_storage_path()
|
|
103
|
+
if token_path.exists():
|
|
104
|
+
with open(token_path, "r", encoding="utf-8") as handle:
|
|
105
|
+
return json.load(handle)
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
logger.error("Failed to load Grok OAuth tokens: %s", exc)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def save_tokens(tokens: Dict[str, Any]) -> bool:
|
|
112
|
+
if tokens is None:
|
|
113
|
+
raise TypeError("tokens cannot be None")
|
|
114
|
+
try:
|
|
115
|
+
token_path = get_token_storage_path()
|
|
116
|
+
with open(token_path, "w", encoding="utf-8") as handle:
|
|
117
|
+
json.dump(tokens, handle, indent=2)
|
|
118
|
+
token_path.chmod(0o600)
|
|
119
|
+
return True
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
logger.error("Failed to save Grok OAuth tokens: %s", exc)
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def exchange_token(token_endpoint: str, body: Dict[str, str]) -> Dict[str, Any]:
|
|
126
|
+
"""POST a form-encoded token request and return the JSON payload."""
|
|
127
|
+
response = requests.post(
|
|
128
|
+
token_endpoint,
|
|
129
|
+
data=body,
|
|
130
|
+
headers={
|
|
131
|
+
"Accept": "application/json",
|
|
132
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
133
|
+
},
|
|
134
|
+
timeout=30,
|
|
135
|
+
)
|
|
136
|
+
if response.status_code != 200:
|
|
137
|
+
raise RuntimeError(
|
|
138
|
+
f"xAI token request failed: {response.status_code} {response.text}"
|
|
139
|
+
)
|
|
140
|
+
return response.json()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def refresh_access_token() -> Optional[str]:
|
|
144
|
+
"""Refresh the access token using the stored refresh token."""
|
|
145
|
+
tokens = load_stored_tokens()
|
|
146
|
+
if not tokens:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
refresh_token = tokens.get("refresh_token")
|
|
150
|
+
if not refresh_token:
|
|
151
|
+
logger.debug("No refresh_token available for Grok OAuth")
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
token_endpoint = tokens.get("token_endpoint") or ""
|
|
156
|
+
token_endpoint = (
|
|
157
|
+
validate_xai_endpoint(token_endpoint)
|
|
158
|
+
if token_endpoint
|
|
159
|
+
else fetch_discovery()["token_endpoint"]
|
|
160
|
+
)
|
|
161
|
+
payload = exchange_token(
|
|
162
|
+
token_endpoint,
|
|
163
|
+
{
|
|
164
|
+
"grant_type": "refresh_token",
|
|
165
|
+
"refresh_token": refresh_token,
|
|
166
|
+
"client_id": GROK_OAUTH_CONFIG["client_id"],
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
new_tokens = tokens_from_payload(payload, token_endpoint, refresh_token)
|
|
170
|
+
if save_tokens(new_tokens):
|
|
171
|
+
logger.info("Successfully refreshed Grok OAuth token")
|
|
172
|
+
return new_tokens["access_token"]
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
logger.error("Grok OAuth token refresh failed: %s", exc)
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_valid_access_token() -> Optional[str]:
|
|
179
|
+
"""Return a valid access token, refreshing if it is (nearly) expired."""
|
|
180
|
+
tokens = load_stored_tokens()
|
|
181
|
+
if not tokens:
|
|
182
|
+
logger.debug("No stored Grok OAuth tokens found")
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
access_token = tokens.get("access_token")
|
|
186
|
+
if not access_token:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
expires_at = tokens.get("expires_at")
|
|
190
|
+
skew = GROK_OAUTH_CONFIG["refresh_skew_seconds"]
|
|
191
|
+
if isinstance(expires_at, (int, float)) and time.time() > expires_at - skew:
|
|
192
|
+
logger.info("Grok OAuth token expired, attempting refresh")
|
|
193
|
+
return refresh_access_token()
|
|
194
|
+
|
|
195
|
+
return access_token
|