Skip to content

Commit ec7e15a

Browse files
fix(service-automation)!: a connector_action call whose input breaks the action's declared inputSchema fails before dispatch, naming each parameter (#28035)
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F <!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuPMZ8BHWtPf6nBTp51qSM64)_ domain:server#2 · os-sales · session_01L6bhgX3dtTQwah2MjCjf5i Fixes #27995 Clause-②: no (narrowing) Metadata-protocol: none Skills-docs: #28041 — `skills/objectstack-automation/SKILL.md`, `skills/objectstack-automation/references/integration-recipes.md` and `content/docs/automation/connectors.mdx` still describe the input as unchecked; filed for after this merges (contract review 6520142501). Sibling call site: the pull door, #28040. ## 维护者速读 - 改了什么:流程里的 `connector_action` 节点在把请求发给连接器之前,先用该动作声明的 `inputSchema` 校验求值后的输入;缺必填参数或类型不对时,这一步失败,错误码 `VALIDATION_ERROR`,逐个点名参数路径并给出改法,请求不发出去。 - 为什么改:以前 `inputSchema` 只是摆设,OpenAPI 的 `getPet` 不传 `petId` 也照样发出去,实测在 main 上发出的是字面量 `/pets/%7BpetId%7D` 且这一步报成功。分诊裁定必须本地响亮拒绝。 - 风险与回滚:收窄(BREAKING):输入不符合动作声明的必填项或类型的调用,以前会发出去,现在在本地失败。只判 `required`、`type`(含 OpenAPI `nullable`)、`properties`、`items`,`$ref`/`oneOf`/`enum`/`format` 等一律不判,避免误拒。仓库内示例与 dogfood 的连接器调用都合规(见盘点)。回滚即还原 `connector-nodes.ts` 一个提交。 - 先看:`packages/services/service-automation/src/builtin/connector-nodes.ts`(派发前的校验块与文件尾的 `collectInputSchemaViolations`)、`src/builtin/connector-input-schema.test.ts`、`.changeset/27995-connector-action-input-schema.md`。 - 两分钟验证:`pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/builtin/connector-input-schema.test.ts`,预期 14 个测试全绿;另有 `pnpm --filter @objectstack/connector-openapi exec vitest run src/input-schema-dispatch.test.ts`,4 个测试全绿。 - 需要你做的:无,席位落地。 ## Measured at the real door (LiteKernel + AutomationServicePlugin + ConnectorOpenApiPlugin) A declared `provider: 'openapi'` connector over `GET /pets/{petId}` (`petId` path parameter, `required: true`, `type: integer`), a flow whose `connector_action` calls `getPet` with `input: {}`, a recording `fetchImpl`. One-off probe, not committed. | | Before (`origin/main`, `connector-nodes.ts` at main) | After (this branch) | |:--|:--|:--| | run | `success: true` | `success: false` | | upstream calls | `["https://petstore.example.com/pets/%7BpetId%7D"]`: the literal placeholder was sent | `[]` | | error | none | `connector_action 'call': input for 'pets.getPet' does not match the action's inputSchema — missing required 'path'. Nothing was sent. Fix connectorConfig.input so it satisfies the inputSchema GET /api/v1/automation/connectors lists for this action.` | ## Inventory (asked for by the order) **1. The `inputSchema` shapes the producers emit.** - `connector-openapi` (`buildInputSchema`): `{ type: 'object', properties: { path, query, header, body }, required }`. Each section is an object schema with its own `required`; `path` is required whenever present; the parameter schemas and the body schema come from the document as written, so they may carry OpenAPI 3.0 `nullable`, `format`, `example`, `enum`, and an unresolved `$ref` when the caller did not dereference the document. - `connector-mcp`: the server's `tools/list` `inputSchema`, passed through verbatim: any JSON Schema draft (often `$schema` 2020-12 or draft-07), commonly with `additionalProperties: false`, `$defs`/`$ref`, `enum`, combinators. - Code connectors: literal object schemas with `type`, `required`, `properties` and per-property `type` (`connector-slack`: `chat.postMessage` requires `channel`, `chat.update` requires `channel` and `ts`, `call` requires `method`; `connector-rest` `request`: no `required`). `connector-imap` declares no connector actions. **2. An existing JSON Schema validator to reuse.** The only one in the repo is `ajv` (+ `ajv-formats`), used privately by `@objectstack/objectql`'s `json_schema` validation rule. It is not reachable from here: `objectql` is a devDependency of `service-automation` (tests only), so importing it at runtime is what `check:undeclared-dep-imports` refuses, and adding `ajv` as a new runtime dependency is outside the claimed file surface (`package.json` + lockfile). It would also be the wrong tool for these schemas: ajv compiles `$ref` eagerly and throws on an unresolved one, rejects an unknown `$schema` draft by default, and does not know OpenAPI 3.0 `nullable`, so it would refuse calls the declared schema permits. No other JSON Schema checker exists in `service-automation` or its runtime dependencies. So the check is a small, deliberate subset written in `connector-nodes.ts` (`collectInputSchemaViolations`): `required`, `type` (type lists, `nullable: true`, `integer` = whole number), and descent through `properties` and `items`. Everything else is not judged and never refuses. If the seat prefers full-draft validation through ajv, that is a dependency change outside this claim. **3. Calls in the repo that this turns into failures.** None found: - `examples/app-showcase/src/automation/flows/index.ts`, the only flows with `connectorConfig`: `slack.chat.postMessage` sends `channel` + `text` (strings, `channel` required ✓); `rest.request` and `showcase_status_api.request` send `method` + `path` (strings, nothing required ✓); `showcase_mcp_tools.echo_upper` sends `text: 'objectstack'` against the fixture's `{ text: string, required: ['text'] }` ✓. `showcase_status_openapi.getHealth` takes no parameters, so it has no `inputSchema`. - Tests that dispatch through the engine with a schema'd action: `connector-mcp`, `connector-rest`, `connector-slack`, `connector-openapi` suites green on this branch (below); the whole `service-automation` suite green; the full dogfood battery green on the final head (below), including `showcase-declarative-mcp.dogfood.test.ts`. **4. Out of scope, as ordered.** No registration-time check: #27968 owns judging a constant `input` at registration through its ledger path. This change judges only at run time, on the evaluated input. ## What changes - **`connector-nodes.ts`**: after `resolveValueSlotMap` evaluates `connectorConfig.input` and before the handler is called, the evaluated input is judged against the action's `inputSchema`, read from `engine.getConnectorDescriptors()` (the same schema `GET /api/v1/automation/connectors` shows the author; it is projected from the stored def unchanged, so no `engine.ts` accessor was added). A failing call returns `{ success: false, code: 'VALIDATION_ERROR', error }`: the error names each problem by dotted path (`missing required 'path.petId'`, `'query.verbose' must be boolean, got string`, `'tags[1]' must be string, got number`), at most five then a count, says nothing was sent, and points at the connectors route. The handler is never called. - **Classification**: a `runtime` failure, not a guard refusal (`refuseNode`): the input can come from run data, so a `fault` edge or `try_catch` can handle it; `$error.code` is `VALIDATION_ERROR`, an existing `StandardErrorCode`, so no `packages/spec` change. - **Not judged**: `$ref`, `oneOf`/`anyOf`/`allOf`, `enum`, `format`, bounds, `pattern`, `additionalProperties`, any `type` name outside the JSON Schema seven, and values with no JSON spelling (a `Date`, a `Map`). An action with no `inputSchema` dispatches whatever it is given. - **Changeset**: `@objectstack/service-automation` patch, BREAKING accept-set narrowing, ADR-0087 `not-required (no-migration-prescription)`. ## Tests `src/builtin/connector-input-schema.test.ts` (new, 11), through `AutomationEngine` + `installBuiltinNodes` with the three producers' schema shapes: 1. Missing required: OpenAPI `input: {}` names `path` (and a `fault` edge receives `$error.code === 'VALIDATION_ERROR'`); `{ path: {} }` names `path.petId` with the fix; two missing are both named; a code connector's `channel`. 2. Wrong type: a literal string for an `integer`; a CEL envelope evaluating to the wrong type (judged on its evaluated value); an array item, by index. 3. Valid calls dispatch: a required value computed by an envelope; OpenAPI `nullable` admits `null`. 4. Unjudged keywords (`enum`, `format`, `$ref`, `oneOf`, extra keys under `additionalProperties: false`) never refuse; an action with no `inputSchema` dispatches an absent input as `{}`. <details><summary>Red before the fix (test committed with the fix; run on the tree before the executor change)</summary> ``` × OpenAPI `getPet` with `input: {}` fails naming `path`, and nothing is sent × `input: { path: {} }` fails naming the nested `path.petId`, with the fix × every missing parameter is named, not only the first × a code connector's required `channel` is enforced the same way × a literal string for an `integer` path parameter names the parameter and both types × an envelope evaluating to the wrong type is judged on its evaluated value × an array item of the wrong type names its index Tests 7 failed | 4 passed (11) ``` </details> <details><summary>Ablation through scripts/ablation-replace.mjs (fix committed first)</summary> ``` anchor " if (violations.length > 0) {" x1 -> x0 replace " if (violations.length > 0 && false) {" x0 -> x1 blob 77ea9e1 -> e1fc7d966b26 (the same 7 tests ×) Tests 7 failed | 4 passed (11) ok restored: blob == HEAD (77ea9e1) and `git diff HEAD` is empty ``` The four over-refusal pins stay green under the ablation, as preservation pins should. </details> ## Verification **Final head `615683fe` (patch round for contract review 6520142501; os-dev-report 6521455883):** derived gates 60/60 exit 0, `--ran` 0 NOT-MEASURED; `service-automation` 224 files / 2860 tests; `connector-openapi` 71, `connector-mcp` 56, `connector-rest` 27, `connector-slack` 10 green; full dogfood battery 357 files passed / 3 skipped, 2875 tests passed / 20 skipped, 0 failed. Contract review delta 6521700736 PASS on this head. The readings below are from the first head 7c7eed0, kept for the record. - `@objectstack/service-automation`: `tsc --noEmit` + `check:test-typecheck` green (a9f9f28); vitest 224 files / 2852 passed + 1 expected fail on 7c7eed0. - `connector-mcp` 56, `connector-openapi` 67, `connector-rest` 27, `connector-slack` 10 tests green against the rebuilt `service-automation` (a9f9f28). - eslint `--no-inline-config` on both changed `.ts` files: exit 0. - `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`: 60 derived, 60 run, all exit 0; `--ran` reconcile `60 run, 0 NOT-MEASURED` (`check-plugin-teardown-shape --self-test` after `git fetch --unshallow`, `check:dual-build-cjs-loads` after building its seven missing dists). - Open PRs on these files: none; `git merge-tree` against #27951, #27964, #27940 clean. <details><summary>Full dogfood battery (narrowing arm), 7c7eed0</summary> ``` pnpm --filter @objectstack/dogfood test (closure built with turbo --filter='@objectstack/dogfood^...') Test Files 357 passed | 3 skipped (360) Tests 2875 passed | 20 skipped (2895) os-verify-lock: VERDICT command-exit 0 · held the lock 1660s (27m40s) ``` </details> ## Acceptance notes - The check judges the subset named above by design; full-draft validation (ajv) would need a new runtime dependency and dereferenced schemas, and is not claimed here. - `inputSchema` is read through `getConnectorDescriptors()`, which builds the descriptor list per dispatch (one map over registered connectors). A dedicated `engine.resolveConnectorActionInputSchema` beside `resolveConnectorActionEffect` would avoid that but is outside the claimed surface (`engine.ts`). --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgX3dtTQwah2MjCjf5i)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgX3dtTQwah2MjCjf5i --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 95f545e commit ec7e15a

5 files changed

Lines changed: 602 additions & 2 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
'@objectstack/connector-openapi': patch
4+
---
5+
6+
fix(service-automation)!: a `connector_action` call whose evaluated input breaks the action's declared `inputSchema` fails the step before dispatch, naming each missing or mistyped parameter, and nothing is sent (#27995)
7+
8+
Clause-②: no (narrowing)
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export, type or error code is removed or renamed: `ConnectorActionSchema.inputSchema` and `connectorConfig.input` keep their shapes, and the refusal reuses the existing `VALIDATION_ERROR` code of the platform vocabulary. What narrows is the run-time dispatch of one node: a call the action's own declared schema says is wrong is no longer sent upstream. Nothing stored is rewritten or re-judged at rest, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->
12+
13+
**BREAKING** (an accept-set narrowing at run time), shipped as `patch`: nothing a metadata author writes changes shape (`Metadata-protocol: none`), so the level follows the kind, and the banner and the ADR-0087 disposition stay.
14+
15+
**FROM.** The `connector_action` node evaluated `connectorConfig.input` and handed it to the connector's handler unchecked. The action's declared `inputSchema` (built from the OpenAPI operation by `connector-openapi`, passed through from `tools/list` by `connector-mcp`, or written by a code connector such as `connector-slack`) was advisory: a call to an OpenAPI `getPet` with `input: {}` reached the remote URL although `petId` is declared `required`, and what came back was the remote's choice.
16+
17+
**TO.** After the input's value slots are evaluated and before the handler is called, the evaluated input is judged against the action's `inputSchema`:
18+
19+
- Judged: `required` at every object level reached, `type` (a type list, OpenAPI 3.0 `nullable: true`, and `integer` as a whole number), and descent through `properties` and array `items`. `null` on an optional property counts as absent, as a provider that serializes a section drops it; `null` on a required property is judged against its type. Text is never converted to a declared number or boolean.
20+
- Not judged, so never refused here: `$ref`, `oneOf` / `anyOf` / `allOf`, `enum`, `format`, bounds, `pattern`, `additionalProperties`, and any `type` name outside the JSON Schema seven. The remote still judges those, as before. An action that declares no `inputSchema` dispatches whatever it is given.
21+
- A failing call fails the step with `code: 'VALIDATION_ERROR'` on `$error`. The error names each problem by its dotted path (`missing required 'path' (with 'path.petId')`, `'query.verbose' must be boolean, got string`, `'tags[1]' must be string, got number`), says nothing was sent, and points at `GET /api/v1/automation/connectors`, which lists the action's schema. The handler is never called.
22+
- The failure is a `runtime` one, not a guard refusal: the input can come from run data, so a `fault` edge or a `try_catch` region can handle it.
23+
24+
**`@objectstack/connector-openapi`.** So that the check never demands what a caller cannot supply, an OpenAPI action's `inputSchema` no longer lists the `Accept`, `Content-Type` and `Authorization` header parameters (OAS 3.0.3 §4.7.12.1 says they SHALL be ignored; the transport and `auth` set them), nor the header or query parameter the connector's `api-key` `auth` fills (`headerName`, default `X-API-Key`, or `paramName`). The credential reaches the request from `auth` as before; a flow never writes it into `connectorConfig.input`. `GET /api/v1/automation/connectors` lists the action without them.
25+
26+
**Who sees it.** A flow or agent whose `connector_action` input omits a parameter the action declares `required`, or sends one of a type the action does not declare: that step now fails before the call instead of the remote answering it. For example, an OpenAPI path parameter declared `type: integer` sent as the string `"7"` (a text field holding an id is the usual source) is refused, where the provider used to interpolate it into the path; the error then says to cast it.
27+
28+
**Fix.** Make `connectorConfig.input` satisfy the action's `inputSchema`: supply every required parameter, and send each one as the declared type (`{ path: { petId: 7 } }`, not `{ path: { petId: '7' } }`). A value computed by a CEL envelope is judged on what it evaluates to, so cast text to the declared type there: `{ path: { dialect: 'cel', source: '{"petId": int(record.vendor_id)}' } }`, with `double()` for a number and `bool()` for a boolean.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* **The `inputSchema` this package builds is the one the `connector_action`
5+
* executor judges before dispatch** (#27995) — pinned on `buildInputSchema`'s
6+
* real output, through a real boot (LiteKernel + AutomationServicePlugin +
7+
* ConnectorOpenApiPlugin), with a recording `fetchImpl` standing in for the
8+
* remote:
9+
*
10+
* 1. `getPet` with `input: {}` fails the step and nothing is sent. Before the
11+
* check it reached the remote as the literal `/pets/%7BpetId%7D`.
12+
* 2. `null` on an optional query parameter is read as absent: the call is
13+
* sent, without that parameter, as omitting it would have been.
14+
* 3. A header or query parameter the connector's `auth` supplies, and the
15+
* `Authorization` header OAS 3.0.3 §4.7.12.1 says is ignored, are not
16+
* demanded of the input although the document declares them required:
17+
* the credential reaches the wire from `auth`, never from the flow.
18+
*/
19+
20+
import { describe, expect, it } from 'vitest';
21+
import { LiteKernel } from '@objectstack/core';
22+
import { AutomationServicePlugin, type AutomationEngine } from '@objectstack/service-automation';
23+
import { ConnectorOpenApiPlugin } from './connector-openapi-plugin.js';
24+
import type { RestAuth } from './openapi-connector.js';
25+
26+
const document = {
27+
openapi: '3.0.3',
28+
info: { title: 'Petstore', version: '1.0.0' },
29+
servers: [{ url: 'https://petstore.example.com' }],
30+
paths: {
31+
'/pets/{petId}': {
32+
get: {
33+
operationId: 'getPet',
34+
parameters: [
35+
{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } },
36+
{ name: 'tag', in: 'query', schema: { type: 'string' } },
37+
{ name: 'api_key', in: 'query', required: true, schema: { type: 'string' } },
38+
{ name: 'X-Api-Key', in: 'header', required: true, schema: { type: 'string' } },
39+
{ name: 'Authorization', in: 'header', required: true, schema: { type: 'string' } },
40+
],
41+
responses: { '200': { description: 'ok' } },
42+
},
43+
},
44+
},
45+
};
46+
47+
type Sent = { url: string; headers: Record<string, string> };
48+
49+
/** Boot a kernel with one hand-wired OpenAPI connector `pets` over {@link document}, recording every request. */
50+
async function boot(auth: RestAuth): Promise<{ engine: AutomationEngine; sent: Sent[]; kernel: LiteKernel }> {
51+
const sent: Sent[] = [];
52+
const fetchImpl = (async (url: string, init: { headers?: Record<string, string> }) => {
53+
sent.push({ url, headers: { ...init?.headers } });
54+
return { status: 200, ok: true, headers: { get: () => 'application/json' }, json: async () => ({}), text: async () => '{}' };
55+
}) as unknown as typeof fetch;
56+
const kernel = new LiteKernel({ logger: { level: 'silent' } } as never);
57+
kernel.use(new AutomationServicePlugin());
58+
kernel.use({
59+
name: 'test.connector-metadata',
60+
type: 'standard',
61+
version: '1.0.0',
62+
dependencies: ['com.objectstack.service-automation'],
63+
async init(c: { registerService(name: string, service: unknown): void }) {
64+
c.registerService('objectql', { registry: { listItems: () => [] } });
65+
},
66+
async start() {},
67+
} as never);
68+
kernel.use(new ConnectorOpenApiPlugin({ document, name: 'pets', auth, fetchImpl } as never));
69+
await kernel.bootstrap();
70+
return { engine: kernel.getService<AutomationEngine>('automation'), sent, kernel };
71+
}
72+
73+
async function callGetPet(engine: AutomationEngine, name: string, input: Record<string, unknown>) {
74+
engine.registerFlow(name, {
75+
name, label: name, type: 'autolaunched',
76+
nodes: [
77+
{ id: 'start', type: 'start', label: 'Start' },
78+
{ id: 'call', type: 'connector_action', label: 'Call', connectorConfig: { connectorId: 'pets', actionId: 'getPet', input } },
79+
{ id: 'end', type: 'end', label: 'End' },
80+
],
81+
edges: [{ id: 'e1', source: 'start', target: 'call' }, { id: 'e2', source: 'call', target: 'end' }],
82+
} as never);
83+
return engine.execute(name, {} as never);
84+
}
85+
86+
describe('connector-openapi inputSchema at the connector_action door (#27995)', () => {
87+
it('1. getPet with input {} is refused naming the path parameter, and nothing is sent', async () => {
88+
const { engine, sent, kernel } = await boot({ type: 'api-key', key: 'k-123', headerName: 'X-Api-Key' });
89+
try {
90+
const result = await callGetPet(engine, 'empty', {});
91+
expect(result.success).toBe(false);
92+
expect(result.error).toContain("missing required 'path' (with 'path.petId')");
93+
expect(sent).toEqual([]);
94+
} finally {
95+
await kernel.shutdown().catch(() => {});
96+
}
97+
});
98+
99+
it('2. null on an optional query parameter is sent as an omission', async () => {
100+
const { engine, sent, kernel } = await boot({ type: 'api-key', key: 'k-123', headerName: 'X-Api-Key' });
101+
try {
102+
const result = await callGetPet(engine, 'null_tag', { path: { petId: 7 }, query: { tag: null, api_key: 'q' } });
103+
expect(result.success, result.error).toBe(true);
104+
expect(sent).toHaveLength(1);
105+
expect(new URL(sent[0].url).pathname).toBe('/pets/7');
106+
expect(new URL(sent[0].url).searchParams.has('tag')).toBe(false);
107+
} finally {
108+
await kernel.shutdown().catch(() => {});
109+
}
110+
});
111+
112+
it('3a. an api-key HEADER from auth, and Authorization, are not demanded of the input', async () => {
113+
const { engine, sent, kernel } = await boot({ type: 'api-key', key: 'k-123', headerName: 'X-Api-Key' });
114+
try {
115+
const schema = engine.getConnectorDescriptors().find((d) => d.name === 'pets')?.actions[0]?.inputSchema as any;
116+
expect(Object.keys(schema.properties)).not.toContain('header');
117+
const result = await callGetPet(engine, 'auth_header', { path: { petId: 7 }, query: { api_key: 'q' } });
118+
expect(result.success, result.error).toBe(true);
119+
expect(sent[0].headers['X-Api-Key']).toBe('k-123');
120+
} finally {
121+
await kernel.shutdown().catch(() => {});
122+
}
123+
});
124+
125+
it('3b. an api-key QUERY parameter from auth is not demanded of the input', async () => {
126+
const { engine, sent, kernel } = await boot({ type: 'api-key', key: 'k-456', paramName: 'api_key' });
127+
try {
128+
const schema = engine.getConnectorDescriptors().find((d) => d.name === 'pets')?.actions[0]?.inputSchema as any;
129+
expect(schema.properties.query.properties).not.toHaveProperty('api_key');
130+
expect(schema.properties.header.required).toEqual(['X-Api-Key']);
131+
const result = await callGetPet(engine, 'auth_query', { path: { petId: 7 }, header: { 'X-Api-Key': 'h' } });
132+
expect(result.success, result.error).toBe(true);
133+
expect(new URL(sent[0].url).searchParams.get('api_key')).toBe('k-456');
134+
} finally {
135+
await kernel.shutdown().catch(() => {});
136+
}
137+
});
138+
});

‎packages/connectors/connector-openapi/src/openapi-connector.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ export function createOpenApiConnector(config: OpenApiConnectorConfig): OpenApiC
214214
key,
215215
label: op.summary ?? titleize(key),
216216
description: op.description,
217-
inputSchema: buildInputSchema(op),
217+
inputSchema: buildInputSchema(op, auth),
218218
outputSchema: buildOutputSchema(op),
219219
});
220220

@@ -318,21 +318,38 @@ function toInfo(op: Op): OperationInfo {
318318
};
319319
}
320320

321+
/**
322+
* Header parameters OAS 3.0.3 §4.7.12.1 says SHALL be ignored, compared
323+
* lower-cased: the transport sets `Content-Type`, and `applyAuth` sets
324+
* `Authorization`, so a value the input supplied could never take effect.
325+
*/
326+
const IGNORED_HEADER_PARAMETERS = new Set(['accept', 'content-type', 'authorization']);
327+
321328
/**
322329
* Assemble the action inputSchema from an operation's parameters + requestBody.
323330
* Produces { type: 'object', properties: { path, query, header, body }, required }
324331
* where only non-empty sections are emitted.
332+
*
333+
* #27995 — the `connector_action` executor now refuses a call whose input
334+
* breaks this schema, so it must not demand what the caller cannot supply:
335+
* the ignored header parameters above, and the header or query parameter
336+
* the connector's own `auth` fills (the credential never belongs in a
337+
* flow's `connectorConfig.input`), are left out of it.
325338
*/
326-
function buildInputSchema(op: OpenApiOperation): JsonSchema | undefined {
339+
function buildInputSchema(op: OpenApiOperation, auth: RestAuth): JsonSchema | undefined {
327340
const sections: Record<'path' | 'query' | 'header', { props: Record<string, JsonSchema>; required: string[] }> = {
328341
path: { props: {}, required: [] },
329342
query: { props: {}, required: [] },
330343
header: { props: {}, required: [] },
331344
};
345+
const authHeader = auth.type === 'api-key' && !auth.paramName ? (auth.headerName ?? 'X-API-Key').toLowerCase() : undefined;
346+
const authQuery = auth.type === 'api-key' ? auth.paramName : undefined;
332347

333348
for (const p of op.parameters ?? []) {
334349
if (!p || typeof p !== 'object' || '$ref' in p) continue;
335350
if (p.in !== 'path' && p.in !== 'query' && p.in !== 'header') continue;
351+
if (p.in === 'header' && (IGNORED_HEADER_PARAMETERS.has(p.name.toLowerCase()) || p.name.toLowerCase() === authHeader)) continue;
352+
if (p.in === 'query' && authQuery !== undefined && p.name === authQuery) continue;
336353
const sec = sections[p.in];
337354
sec.props[p.name] = p.schema ?? (p.description ? { type: 'string', description: p.description } : { type: 'string' });
338355
if (p.required) sec.required.push(p.name);

0 commit comments

Comments
 (0)