Skip to content

Integrations, 2026-09-01/02: sub-agents, GitHub (per-user MCP), Google Drive

Process and results of the two-day integrations push on mit.nonlocally.org. Everything here was measured (a command, a probe, a live click) unless marked unverified. Owner decisions are recorded where they shaped the outcome. Tracking issue: #442.

TL;DR

capability state how a user gets it evidence
Tool calling for every model fixed nothing to do 17/32 models TOOL CALL under FORCED tool_choice (was 13); grok-4.6 and workspace-agent flipped from prose
Sub-agents (delegate_task) live on code-agent, code-reviewer only pick one of those presets two parallel delegations returned 697 and 4096, attributed to sub-agents
GitHub (issues, PRs, repo files) as the acting user live, opt-in per chat in a chat: integrations icon → Tools → GitHub on; authorize once get_me → the user's own login; add_issue_comment posted as the user; PRs listed from the real chat UI
Google Drive picker wired; Google shows an unverified-app screen + → Google Drive → Advanced → continue client id + API key live, JS origin added; blocked by OpenWebUI's drive.readonly scope (upstream issue filed)

Stays within OpenWebUI's own features: every change is reachable from the admin UI. The repo's models.yaml + scripts/sync_*.py are the declarative mirror of the same endpoints.

Process (what was actually done, in order)

  1. Probe before believing. A FORCED-tool_choice probe (scripts/probe_tool_calling.py, run inside the litellm pod) measured every offered model. Only the forced form is deterministic against this proxy; auto flip-flops on unchanged config.
  2. Root cause, in our own code. grok-4.6 "could not call tools" because k8s/litellm/tool_policy.py, our LiteLLM pre-call hook, stripped tools for any alias whose name did not start with a listed prefix. Not upstream LiteLLM, not the model. Fixed config-driven (#461 → #462): capability now comes from model_info.supports_function_calling or the upstream provider, and every strip is logged. Deployed with scripts/deploy_litellm.sh. Record: tests/data/tool_calling_probe.json, pinned to the hook's sha (tests/test_goal_20260901.py).
  3. Sub-agents, safely. OpenWebUI v0.11.2 gates delegate_task per model with meta.builtinTools.subagents, default true, AND a global toggle. So the toggle alone would have enabled delegation on all 37 visible presets (it briefly did; turned off again). Now: models.yaml builtin_tools: (defaults deny, coding bundle grants), sync_models.py renders/drift-checks it, and scripts/sync_subagents.py refuses to flip the toggle while any visible record-backed model lacks the opt-out (#463, #464). E2E verified in the UI.
  4. GitHub as a standard connector. Options researched (shared PAT, login-token pass-through, community Workspace tools, mcpo + archived stdio server, repo-as-context) and rejected in favour of GitHub's official remote MCP server (https://api.githubcopilot.com/mcp/) as an admin External Tool Server with OAuth 2.1 (Static) backed by the org's GitHub App mit-nonlocally-openwebui (App 4804367): each user authorizes once and acts as themselves; no third-party code in the pod; no tokens pasted into chat. Verified live from the UI and API. Runbook and options memo: #442 comments.
  5. Google Drive. First-party integration: GOOGLE_DRIVE_CLIENT_ID/GOOGLE_DRIVE_API_KEY via ExternalSecret (#467), toggle in Admin › Documents, and the OAuth client's missing JavaScript origin https://mit.nonlocally.org added in the Cloud console.
  6. Spec as tests. tests/test_goal_20260901.py and tests/test_goal_20260902.py hold the criteria; still-red items are xfail(strict=True) with the measured reason, never deleted.

Decisions (owner)

  • Auth pane unchanged: merge-accounts-by-email off, auto-redirect off, LDAP off, keep Google + GitHub login.
  • Sub-agents: coding presets only, background mode on.
  • GitHub: new GitHub App with per-user OAuth, toolsets issues + pull_requests + repos, and opt-in per chat, not a preset default (a default auto-redirects a user's first new chat to GitHub consent). Coding prompts tell the model how to ask the user to enable it.
  • Prefer official/vendor integrations over community plugins or custom code.

How to operate

# declared vs live, then apply (all read-only until --apply)
python3 scripts/sync_models.py                 # presets, builtinTools opt-outs, prompts
python3 scripts/sync_tool_servers.py           # external tool servers (github: OAuth material carried from live)
python3 scripts/sync_subagents.py              # global toggle; refuses to enable while a visible model could leak
python3 scripts/probe_tool_calling.py --list   # the offered-model set the probe covers
Order when enabling delegation: sync_models.py --apply (opt-outs) before sync_subagents.py --apply. GitHub's OAuth client blob is created only by Register Client in Admin › Integrations; the sync never renders it and fails closed if the server is not registered. Production detection is scheduled by k8s/model-served-base-check-cronjob.yaml; it runs sync_models.py in plan-only mode and alerts through the existing CronJob-failure channel only when newly unresolvable declaration ids appear vs the previous snapshot.

Secrets (all in GCP Secret Manager, never in the repo): openweb-marimo-github-mcp-client-id, -client-secret, -private-key, openweb-marimo-google-drive-api-key.

Gotchas we hit

  • OpenWebUI's per-model builtin-tool default is true; a global toggle is never enough.
  • sync_tool_servers.py --apply replaces the whole connection list; an undeclared live connection is retired. Declare before you apply.
  • A LiteLLM fallback can answer for a pinned model: gpt-5 TOOL CALLs were served by claude-sonnet-4-6 while OpenAI had no credits. The probe records served_model.
  • Base-model records can be deactivated (happened 2026-09-01 20:40Z, unattributed): presets on them return "Model not found". sync_models.py reports them as "not served by any connection". Recovery runbook (admin token): POST /api/v1/models/model/toggle?id=<MODEL_ID> to re-activate each flipped base record (for example claude-sonnet-4-6, claude-haiku-4-5, [local] Qwen3 235B).
  • Reconciler attribution lines are tagged [reconciler-audit]: reconciler-made restores/hides are actor=model-health-reconciler, and externally observed flips include OpenWebUI updated_by* actor hints in the event detail.
  • Google's Drive picker needs the OAuth client's JavaScript origin, not just redirect URIs; OpenWebUI requests drive.readonly (restricted) so an unverified app shows Google's warning and is capped at 100 users. Upstream: open-webui/open-webui#29537.
  • The Claude Code in-app browser could not render OpenWebUI while its IndexedDB backend was wedged ((app)/+layout.svelte awaits openDB('Chats')); a desktop-app restart fixed it. Upstream: open-webui/open-webui#29538.
  • tests/goal/ spawns a real Kubernetes Job; it is not a local regression suite.

Open

  • OpenAI account has no credits (6 openai/* routes 429); two strict xfails flip when fixed.
  • #468 glm has no backend; #470 model_health_reconciler still opens the dead sqlite file.
  • Google Drive: Google verification of drive.readonly, or upstream scope change.
  • PR #469: goal-state 2026-09-02 + the GitHub declaration in models.yaml.