Skip to content

feat(app-shell): the connector Input section derives typed fields from the descriptor's inputSchema (#4305) - #4572

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4305-connector-input-schema
Aug 13, 2026
Merged

feat(app-shell): the connector Input section derives typed fields from the descriptor's inputSchema (#4305)#4572
yinlianghui merged 1 commit into
mainfrom
claude/issue-4305-connector-input-schema

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #4305

A committed connector action published its input contract and the designer ignored it. GET /api/v1/automation/connectors serves each action's inputSchema, and nothing in app-shell read it, so the Input section stayed a generic untyped key/value repeater. The connector and action pickers were correct throughout — this was the last untyped step of that flow.

Both ends measured first (ruling step 1)

(a) The descriptor's schema language. ConnectorActionDescriptor.inputSchema is typed Record< string, unknown > and documented as JSON Schema (@objectstack/spec integration/connector-descriptor.ts:53); the engine projects it verbatim from the connector's authored ConnectorActionSchema.inputSchema (service-automation/src/engine.ts:2122, inputSchema: a.inputSchema). Every shipped connector emits a plain object schema:

connector shape
slack {type:'object', required:['channel'], properties:{…}} — string / array
rest {type:'object', properties:{ method, path, headers:{type:'object'}, query, body:{} }}
openapi {type:'object', properties:{ path/query/header:{type:'object',properties}, body }, required}
mcp the MCP tool's own inputSchema, passed straight through

required is the JSON-Schema array at object level. ⚠️ This is a different contract from the flow-node dialect of the same word: flow.nodes[].inputSchema (spec automation/flow.zod.ts:335) is Record< string, {type, required, description} >, a per-key required: true map. Only the descriptor one is read here.

(b) Today's Input section. flow-node-config.ts:815at('connectorConfig', 'input', 'Input', 'keyValue', …), i.e. one keyValue field at path ['connectorConfig','input'], rendered by FlowKeyValueField. Storage shape is the spec's input: z.record(z.string(), z.unknown()). mergeServerFlowFields always preserves that sibling-block field, so it survives every engine-published configSchema (framework#4210).

One resolver, no new interpreter (ruling step 2)

json-schema-to-fields — the resolver the inspector already uses for a node type's published configSchema — speaks exactly this language, so the descriptor's schema goes through it unchanged. The new adapter only re-roots what comes back: that resolver hard-roots every field under the config root (one path segment per schema property), while a connector's inputs live under connectorConfig.input, which is what the executor reads. It rewrites the path, prefixes the id, and follows showWhen controller references onto their re-rooted ids.

A property the resolver declines is not claimed either — an array with no items (Slack's blocks), a bare {type:'object'} (REST's headers), a union — so it stays repeater-editable and no descriptor can make a stored key unreachable.

Round-trip and additionalProperties (ruling step 3)

Typed fields read and write the same connectorConfig.input map an existing flow already committed, key by key — editing one input leaves every other key, declared or not, at its stored value and position.

additionalProperties was measured, not assumed: no shipped connector emits the key at all, JSON Schema's default for an absent one is open, and the executor confirms it — connector-nodes.ts:107 calls handler((cfg.input ?? {}) as Record< string, unknown >, handlerCtx), passing the whole map through unvalidated. So undeclared keys really are accepted, and the rule followed is the JSON-Schema rule: closed iff additionalProperties === false, open otherwise.

  • Open → typed fields plus the repeater, trimmed via omitKeys to exactly the keys the typed fields do not own. Its commit merges back over the stored map rather than replacing it (the widget's own commit replaces whatever it is handed, so without this one extras edit would wipe every typed input).
  • Closed → the repeater is dropped — unless the stored map still holds undeclared keys. Hiding config an older flow committed would be worse than offering an editor the new schema no longer invites, and this package's standing rule is that a field showing a stored value is never hidden.

Red-first (ruling step 4)

Predicted split written before running; 7 red / 7 green on unfixed code, matching. Two assertions initially passed and were strengthened rather than kept: a bare getByDisplayValue('C123') probe was satisfied by the untyped repeater's own cell, so it could never go red — it now goes through the field label instead.

Reverse verification (fix removed via git checkout + module moved aside, never git stash; all five files restored and sha256-verified): 8 red / 6 green. The one-test delta from the prediction is honest and explained — the extras-edit test became fix-dependent after I added its settling wait.

Pins green on both sides: no-schema descriptor keeps the byte-identical repeater, an unreachable registry keeps it, a node with no action committed keeps it, an array-shaped input is left wholly alone, and the picker behaviours (registry diff, undispatchable refusal, provenance annotation) are untouched with their existing suites green.

i18n and versioning (ruling step 5)

Labels and help come from the descriptor's own title / description — the connector's own channel, meta() in the shared resolver — so no UI copy keys were added. The extras repeater keeps its existing localized "Input" label, which is why the form is applied after localization: typed fields must not be overlaid from the client's zh table.

.d.ts diff measured both ways from clean dist/ + tsconfig.tsbuildinfo: one new internal module, one added optional property (omitKeys?: string[]), nothing removed or narrowed. Neither reaches the package entry (dist/index.d.ts exports neither FlowConfigField nor the new module), so this is internal surface → patch (#4496 precedent).

Verification

  • vitest flow-builder + inspectors + previews + flow-envelope ratchet: 87 files, 1009 passed, 1 skipped
  • pnpm --filter @object-ui/app-shell type-check (both tsc passes: tsc --noEmit and tsc -p tsconfig.test.json) green
  • eslint vs the exact branch base (794dd1c) in a comparison worktree: 2373 = 2373, NET ZERO new warnings. Two warnings this change first introduced were fixed rather than absorbed — a synchronous setState in an effect, and a React Compiler "existing memoization could not be preserved" caused by node?.type inline inferring a dependency on all of node
  • check:phantom-deps, check:control-bytes green; grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' over every created/edited file clean

Not built, reported instead

JSON Schema required is not represented. FlowConfigField has no requiredness concept, so the typed fields cannot mark it; the engine reads it at dispatch. Building that would have meant extending the shared field vocabulary, which is beyond this card.

Scope respected: only the flow-builder connector-node files, one new adapter module, new tests, and the changeset. The inspector/host files from #4536 / #4547 / #4558 were checked and are untouched by this change.


Generated by Claude Code

…m the descriptor's inputSchema (#4305)

A committed connector action published its input contract and the designer
ignored it: `GET /api/v1/automation/connectors` serves each action's
`inputSchema` (the connector's own JSON Schema, projected verbatim by the
engine) and nothing in app-shell read it, so the Input section stayed a
generic untyped key/value repeater.

The mapping reuses `json-schema-to-fields` — the resolver the inspector
already uses for a node type's engine-published `configSchema`, which speaks
exactly this language. A small adapter only RE-ROOTS its output: the resolver
hard-roots every field at `config.<key>`, while a connector's inputs live in
the spec-structured `connectorConfig.input` block the executor reads. No
second JSON Schema interpreter is introduced, and a property the resolver
declines stays repeater-editable so no descriptor can make a key unreachable.

Round-trip is the binding constraint: typed fields read and write the SAME
stored map, key by key, so editing one input leaves every other key at its
stored value and position. `additionalProperties` is followed as measured —
absent in every shipped connector, and JSON Schema's default for absent is
open, which the executor confirms by passing the map to the handler
unvalidated. Open keeps the repeater beside the typed fields for the extras
only, merging its commit back instead of replacing the map; closed drops it,
unless stored undeclared keys would otherwise be hidden.

Descriptors with no inputSchema, an unreachable registry, no action committed
yet, and an array-shaped input all keep the repeater unchanged. Labels come
from the descriptor's own title/description, so no UI copy was added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 13, 2026 11:58am

Request Review

@github-actions github-actions Bot added the tests label Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-DFPi5DWy.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 489.32KB 108.45KB
core (index.js) 3.37KB 1.34KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 163.56KB 44.83KB
fields (index.js) 230.37KB 57.17KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.86KB 12.91KB
plugin-charts (index.js) 62.10KB 17.67KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 120.99KB 31.55KB
plugin-designer (index.js) 212.58KB 42.83KB
plugin-detail (index.js) 239.88KB 59.99KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.30KB 40.02KB
plugin-grid (index.js) 189.37KB 50.33KB
plugin-kanban (index.js) 52.74KB 14.53KB
plugin-list (index.js) 111.13KB 27.12KB
plugin-map (index.js) 18.16KB 5.81KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.16KB 10.96KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.08KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.73KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

PM step-7 复核 — ACCEPT (session_017Qqyix2QcnpUC9XeYVDzx3)

  • The measurement set answers every question the ruling asked and two it didn't know to ask: the schema language pinned at four ends (spec declaration, authoring zod, engine projection VERBATIM, four real producers), the required name collision between the JSON-Schema array and the flow-node per-key dialect disambiguated before it could mislead, and the OPEN verdict proven against the runtime's actual dispatch (cfg.input ?? {} passed verbatim and unvalidated) rather than the documentation.
  • The adapter is the one-resolver ruling executed exactly: jsonSchemaToFlowFields reused UNCHANGED — the "new interpreter" STOP never approached — with re-rooting as the only new logic, and declaredKeys derived from EMITTED fields so a shape the resolver declines stays repeater-editable and no descriptor can strand a stored key. The FlowKeyValueField.flush() replace-the-map hazard was found by measurement and is precisely why the filter-down/merge-back design exists; the order-preserving fold-back pin makes the round-trip contract byte-honest.
  • The closed-schema exception (extras repeater survives when the stored map holds undeclared keys) correctly subordinates the schema to the package's standing "a stored value is never hidden" rule.
  • Red-first with the strengthening correction named: two assertions that could never red (getByDisplayValue satisfied by the repeater's own cell) were rerouted through field labels instead of kept as false evidence. The reverse-run's one-test column move is explained, not smoothed. The three timing failures were investigated to ground with instrumentation (restored sha256-verified) and proven harness-side before any wait was added — no product code changed to make a test pass.
  • Net-zero lint achieved the right way: both self-introduced warnings FIXED, including the React Compiler memoization decline root-caused to an inline node?.type widening the inferred dependency. dts internal-only ⇒ patch per fix(app-shell): organization & invitation UI translates its six English holdouts (#4474) #4496. The JSON-Schema required gap (FlowConfigField has no requiredness concept) is reported for a future card rather than invented. CI 20/20 on per-job conclusions.

Auto-merge armed (squash) — landing verified per the merge-queue discipline.


Generated by Claude Code


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 13, 2026 12:08
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 13, 2026
Merged via the queue into main with commit 082ca7b Aug 13, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4305-connector-input-schema branch August 13, 2026 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

flow-connector-picker: a committed action's Input section stays a generic untyped key/value repeater — the descriptor's inputSchema is never consumed

2 participants