Connect-owned Open WebUI roles¶
Required behavior¶
An opt-in Connect browser resource can manage the corresponding native application's member/admin role. Home exposes those choices while retaining passthrough routing. CLI grants and Home edits share the existing authoritative human record. Changes converge through the existing account reconciliation worker; no second poller or global administrator-group mapping is introduced.
For Open WebUI 0.12, member maps to user, admin to admin, and absent grant, disabled identity, or pending deletion maps to pending. A grant without a linked OAuth account reports awaiting linkage. First browser login establishes that link. Native clients reject pending roles. Existing app data is retained.
Architecture and custody¶
The public manifest gains an optional application_role_bindings list, each with resource, adapter_path, adapter_sha256, configuration_path, configuration_sha256. Bindings reference existing browser passthrough resources; paths are absolute and installed outside writable checkouts. Root verifies regular files, no symlinks/hardlinks, trusted ancestors and exact digests before invoking fixed /usr/bin/python3 -I with no shell, bounded stdin/stdout/stderr and deadline. No secrets or identities are printed in ordinary result summaries.
The strict stdin contract is schema anvil-connect.application-roles/v1, mode check or apply, resource, issuer, configuration_path, configuration_sha256, complete true, and accounts. Each account contains subject, principal, generation (canonical positive decimal string; null only for confirmed absent authority), role (member/admin/none). Identity comes from the managed IdP's protected subject export and exact Connect principal inspection, never email. Disabled or removed grants appear as none. A complete roster's omissions may revoke only previously adapter-owned identities. Persist protected mapping receipts to recover deletion and lost replies. Re-read authority immediately before mutation and detect changed authority afterward; converge on the next bounded sweep rather than claim distributed atomicity.
AI-Infra owns the pinned WebUI adapter and its managed installation/configuration. It runs on the local host against an exactly verified container/image/storage, using the established in-container administrative API adapter pattern. The application owner's first admin is read via the supported model layer solely to create an ephemeral in-process administrative credential. That credential never leaves the container or appears in output or files. User inventory, exact OAuth-sub matching, role-only updates, and session revocation use supported native HTTP APIs. Do not enable API keys, change passwords, read arbitrary user data, or write application SQL.
The adapter validates effective issuer/provider, disabled OAuth role mapping, pending default role and relevant API-key policy through an allowlisted probe. It never dumps OAuth/admin configuration or the environment. Managed activation changes only necessary owned settings with protected rollback; preserve exchange flags, image, mounts and other settings.
Reconciliation and failure rules¶
Preserve the primary recovery administrator. Activation requires its exact Connect identity to have an explicit application admin grant; refuse unknown/duplicate subjects and unsafe adoption, including existing admins without matching authority. No admin authority comes from global groups. Default configuration stays unchanged for deployments without a binding.
Updates use role-only user update, then account-wide session revocation on reductions (prefer all role changes for consistent socket refresh). Revoke rotates the application token stamp and disconnects sockets; it does not claim to erase upstream OAuth session metadata. Persist a forward-recovery intent before changing role. If revocation fails, leave the reduced role, report incomplete reconciliation and retry; never roll privileges upward. Never demote the primary or last administrator. Avoid stale-claim re-promotion by keeping OAuth role mapping disabled.
Role reconciliation runs before account deletion. A role reconciliation error blocks destructive deletion so a failed native revocation cannot lose its subject authority; operator-group reconciliation remains independent. Pending first-login linkage alone is not an error. Phase results distinguish failed, blocked, pending, and completed. Receipt mappings are recovery evidence, never permission authority. Mark completion only after readback and required revocation, including ambiguous/lost replies. Existing timer cadence is 10 seconds after the previous run, with a 120-second outer deadline. Privilege changes are eventually consistent; document and measure observed lag rather than promising instantaneous revocation.
Verification¶
Meaningful tests cover strict manifest/protocol/custody bounds, opt-out unchanged, subject mismatch/duplication, global groups do not confer authority, Home metadata and unchanged routing, create/admin/member/no-access/suspend/delete, first-login pending, native pending rejection, primary/last-admin adoption, role-update/revoke failure and lost-reply retry, no-op repeat, drift and concurrent authority changes, sanitized output, live managed preview/apply/repeat and application HTTP/socket revocation behavior. Independent review must pass before deployment.
Adapter wire contract and bounds¶
Input is exactly one ASCII JSON object (sorted keys, compact separators, trailing newline), at most 64 KiB. The schema is anvil-connect.application-roles/v1; fields are schema, mode, resource, issuer, configuration_path, configuration_sha256, complete, accounts. The complete roster supports at most 64 local accounts/public OpenID subjects. It is never silently truncated. No email, password, group, bearer token, or account name is sent to the adapter.
The exact result schema is anvil-connect.application-role-result/v1, with fields schema, resource, mode, request_sha256, checked, changed, pending_first_login, retired, sessions_revoked, and failed. request_sha256 hashes the exact stdin bytes, including newline. Every count is an integer in 0…4096; checked equals the input account count plus retired, and each other count is at most checked. Retired entries are previously adapter-owned mappings absent from the complete request. failed > 0 or nonzero exit is incomplete reconciliation. Preview changed reports the proposed count; apply reports verified changes. The normal CLI exposes only resource IDs and counts, not request hashes or account identifiers.
Each adapter runs at most 45 seconds with separate 16 KiB stdout/stderr caps and a killable process group. The role phase has a 90-second deadline, including managed lock acquisition and native authority subprocesses. Source/configuration and subject authority are checked immediately before invocation and after completion. An authority change after apply reports a partial result and blocks deletion; it does not replay or roll privileges upward. The outer account service retains its 120-second bound.
Adapter/configuration files are root-owned, single-link regular files with safe root-owned ancestors outside Git worktrees, and exact declared hashes. Configuration is mode 0600. The invocation is fixed /usr/bin/python3 -I ADAPTER; stdin selects check/apply. The minimal process environment has no ambient credentials. Adapter installation must keep locks/receipts in an explicitly supported writable path outside home directories; the existing worker protects home directories and does not weaken that sandbox.