feat(builder): add oauth_providers descriptor + type:oauth wiring for AgentSpec#390
feat(builder): add oauth_providers descriptor + type:oauth wiring for AgentSpec#390sneumannb5 wants to merge 5 commits into
Conversation
ConnysCode
left a comment
There was a problem hiding this comment.
Review: Request changes — two changes before merge; the core is sound.
The schema/codegen/linter that closes #371 is correct and well-covered. I cloned the head, built the workspace, and ran the relevant suites: OAuth schema + linter tests pass 10/10 and codegen.test.ts passes 47/47, including the e2e test that feeds the generated manifest through the real loadManifestFromPath — so there's no drift between the builder schema and extractOAuthProviders. I also confirmed OAuthProviderSchema faithfully mirrors OAuthProviderDescriptor (api/admin-v1.ts) and the loader (pkce default, token_auth_style enum, client_*_field gating all line up), and that patchSpec is a hard gate (spec isn't persisted on lint failure), so codegen only runs on a validated spec. The required changes below are about not shipping dead/loose surface alongside that core.
Must change before merge
🟠 Don't ship the dead web-ui surface. OAuthProvider, AgentSpecSkeleton.oauth_providers, and the new 'oauth' member of SetupField['type'] (web-ui/app/_lib/builderTypes.ts) are referenced only by each other — no component constructs or reads them, and the manual SetupFieldsEditor dropdown (SetupFieldsEditor.tsx:23) still offers only string|secret|url|number|boolean, so an operator can't author a type:oauth field in the UI. Either wire these types into the builder UI in this PR (add 'oauth' to FIELD_TYPES + render provider/scopes inputs), or remove them and land them with the UI follow-up. As-is they're unused exports.
🟠 Gate the provider/scopes manifest forward on type:oauth. mapSetupFieldSpecToManifest (codegen.ts:590-591) forwards provider/scopes for any field type, and the Zod schema (agentSpec.ts:145-146) allows them on every type, so a non-oauth field carrying a stray provider is emitted into the manifest. Add field.type === 'oauth' to the forward (and/or a linter rule rejecting provider/scopes on a non-oauth field) so the schema's stated "type:'oauth' only" contract is actually enforced.
PR description — factcheck
| Claim | Reality |
|---|---|
| "middleware … builder 57/57; 5 pre-existing fails env-only" | Couldn't reproduce the exact counts (my clone lacks built workspace dist/, so ~43 route/integration tests fail on missing @omadia/* builds — environmental, not real). The PR-relevant unit suites I could run pass: oauth schema/linter 10/10, codegen 47/47. |
| "web-ui … vitest (169/169)" | Not run (web-ui deps not installed); please keep this current. |
"Shape: uses real kernel contract … not the issue's guessed field_key/authorization_url" |
Confirmed — schema matches OAuthProviderDescriptor + the loader; the e2e test proves the generated manifest arms the broker. |
| "Additive schema only … No migration/API/CI/env change" | Confirmed for middleware; oauth_providers defaults [] and is omitted from the manifest when empty. |
Minimum to merge
- Resolve the dead web-ui types (wire into the UI, or drop until the UI lands).
- Gate the
provider/scopesforward ontype:oauth.
Minor / nits (non-blocking)
agentSpec.ts:446— the "keep all three in sync" comment really tracks five hand-mirrored copies of this shape (Zod schema,admin-v1.ts, loader,_lib/storeTypes.ts,_lib/builderTypes.ts);admin-v1.ts's header already calls for promoting it to a shared package. Please file a follow-up rather than normalizing manual sync.web-ui/app/_lib/builderTypes.ts:143—pkce?: booleanis optional here, but the kernel contract (admin-v1.ts:106) andstoreTypes.tshavepkce: boolean(required). Tighten the mirror.- Test coverage gaps: no case for multiple
type:oauthfields referencing distinct providers, none forprovider/scopeson a non-oauth field, andextra_authorize_paramsis never round-tripped through codegen+loader.
(Reviewed against head 2c0f40e.)
| // Spec 005 (#371) — inert authorization-code descriptor consumed by the | ||
| // kernel OAuth engine; no plugin code touches the flow. Mirrors | ||
| // OAuthProviderDescriptor in api/admin-v1.ts + manifestLoader. | ||
| // extractOAuthProviders — keep all three in sync (pkce defaults true, |
There was a problem hiding this comment.
Non-blocking, but flag the sync debt. This comment says "keep all three in sync," but the shape is now hand-mirrored across five places: this Zod schema, OAuthProviderDescriptor (api/admin-v1.ts), extractOAuthProviders (manifestLoader.ts), and two web-ui interfaces (_lib/storeTypes.ts, _lib/builderTypes.ts). The admin-v1.ts header already documents the intended fix (promote the canonical type to a shared package — @omadia/plugin-api is the natural home, with the Zod schema satisfies-ing it). Please file a follow-up issue so manual sync doesn't become the norm.
There was a problem hiding this comment.
Still open after this push — non-blocking. The shape is now hand-mirrored across ~6 places (this Zod schema, admin-v1.ts OAuthProviderDescriptor, manifestLoader/extractOAuthProviders, _lib/storeTypes.ts, and the new _lib/builderTypes.ts OAuthProvider). Please file a follow-up to promote the canonical type to the shared package admin-v1.ts already points at (Zod satisfies-ing it), rather than letting manual sync become the norm. Not blocking this PR. See my original comment above.
ConnysCode
left a comment
There was a problem hiding this comment.
Review: Approve
Both changes I requested last round are done, and the middleware core is still green — I re-cloned the head, built @omadia/plugin-api, and ran the two relevant suites: 62/62 pass (agentSpecOAuthProviders + codegen, incl. the e2e that feeds the generated manifest through the real loadManifestFromPath — so no drift between the builder schema and extractOAuthProviders).
Already changed since the last review
- Web-ui surface is now wired, not dead.
SetupFieldsEditortakesoauthProviders,'oauth'is inFIELD_TYPES, and atype:oauthrow renders a provider<select>+ scopes input;SpecEditorpassesspec.oauth_providers ?? [](SpecEditor.tsx:311). The previously-unusedOAuthProvider/oauth_providers/'oauth'exports are now consumed. Blast radius checked: the builderSetupFieldunion is only switched on inside the builder editor — the operator-facing config renderer (admin/settings/page.tsx) uses a separate type, so adding'oauth'doesn't reach it. provider/scopesforward is gated ontype:oauth.mapSetupFieldSpecToManifestnow guards the forward withfield.type === 'oauth'(codegen.ts:592), and the linter addssetup_field_provider_on_non_oauthto reject a strayprovider/scopeson a non-oauth field up-front. The "type:'oauth'only" contract is enforced end-to-end; covered by the two new codegen cases + two linter cases.- Nit closed:
pkceis nowpkce: boolean(required) inbuilderTypes.ts, matching the kernel contract.
Verified while I was in there: the full-array replace on /setup_fields (SetupFieldsEditor.tsx:65) means switching a row away from oauth genuinely drops the old provider/scopes server-side (not just the belt-and-suspenders undefined in the patch), so no stale-provider field can survive a type change. The scopes local-buffer / adjust-state-on-prop-change logic handles the in-progress-separator and external-edit cases correctly.
PR description — factcheck
| Claim | Reality |
|---|---|
| "middleware: oauth + codegen 62/62, typecheck clean" | Confirmed 62/62 (built @omadia/plugin-api, ran both suites). Typecheck not independently run. |
| "e2e: manifest → real loader arms broker + ctx.oauthTokens; extra_authorize_params round-trips" | Confirmed — the loader e2e + extra_authorize_params round-trip tests are part of the 62 and pass. |
| "web-ui: lint + typecheck + vitest 169/169, i18n OK" | Not run here (web-ui deps not installed). i18n spot-check: all 5 oauth.* keys used by the component exist in both de.json + en.json. Please keep the counts current. |
| "Additive schema — oauth_providers defaults [], omitted when empty" | Confirmed (omits oauth_providers … when the spec declares none test passes). |
Optional nits (non-blocking)
SetupFieldsEditor.tsx:281— the provider<label htmlFor={providerId}>always renders, but whenoauthProvidersis empty the<select id={providerId}>is replaced by a<p>, so the label points at a non-existent control. Minor a11y dangle; consider droppinghtmlFor(or wrapping the label) in the no-providers branch.SetupFieldsEditor.tsx:289— iffield.providerreferences a descriptor id that was later removed, the<select value=…>has no matching<option>and silently renders empty. The linter'soauth_field_provider_unresolvedcatches it on save, so functionally fine; a stale-value hint would be nicer UX. Non-blocking.- Sync debt (my earlier nit) — the descriptor shape is now hand-mirrored across ~6 places (Zod,
admin-v1.ts, loader,storeTypes.ts,builderTypes.ts). Still worth a follow-up to promote it to the shared packageadmin-v1.tsalready points at, rather than normalizing manual sync. Non-blocking; see the thread I left open.
(Reviewed against head 5dda400.)
What
oauth_providers[] + provider/scopes on type:oauth fields, wired schema → codegen →
linter → setup-fields UI. Closes #371.
Why
type:oauth was dead: descriptor had nowhere to live (ctx.oauthTokens undefined), no
UI to author it. Now codegen emits it (gated on type:oauth), linter checks wiring +
rejects provider/scopes on non-oauth fields, editor renders provider select + scopes.
Test plan
middleware: oauth + codegen 62/62, typecheck clean
web-ui: lint + typecheck + vitest 169/169, i18n OK
e2e: manifest → real loader arms broker + ctx.oauthTokens; extra_authorize_params round-trips
Risk
Additive schema (oauth_providers defaults [], omitted when empty). Codegen gate drops
stray provider on non-oauth — test-covered. No migration/API/CI/env change.