Repository navigation
Commit ec7e15a
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
- .changeset
- packages
- connectors/connector-openapi/src
- services/service-automation/src/builtin
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
217 | | - | |
| 217 | + | |
218 | 218 | | |
219 | 219 | | |
220 | 220 | | |
| |||
318 | 318 | | |
319 | 319 | | |
320 | 320 | | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
321 | 328 | | |
322 | 329 | | |
323 | 330 | | |
324 | 331 | | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
325 | 338 | | |
326 | | - | |
| 339 | + | |
327 | 340 | | |
328 | 341 | | |
329 | 342 | | |
330 | 343 | | |
331 | 344 | | |
| 345 | + | |
| 346 | + | |
332 | 347 | | |
333 | 348 | | |
334 | 349 | | |
335 | 350 | | |
| 351 | + | |
| 352 | + | |
336 | 353 | | |
337 | 354 | | |
338 | 355 | | |
| |||
0 commit comments