Skip to content

fix(service-automation)!: a connector_action call whose input breaks the action's declared inputSchema fails before dispatch, naming each parameter - #28035

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-9c0do1
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-9c0do1

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Requested by Sales · project thread

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 {}.
Red before the fix (test committed with the fix; run on the tree before the executor change)
× 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)
Ablation through scripts/ablation-replace.mjs (fix committed first)
anchor "            if (violations.length > 0) {"  x1 -> x0
replace "            if (violations.length > 0 && false) {"  x0 -> x1
blob 77ea9e1dbb89 -> e1fc7d966b26
(the same 7 tests ×)
Tests  7 failed | 4 passed (11)
ok restored: blob == HEAD (77ea9e1dbb89) and `git diff HEAD` is empty

The four over-refusal pins stay green under the ablation, as preservation pins should.

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.

Full dogfood battery (narrowing arm), 7c7eed0
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)

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

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgX3dtTQwah2MjCjf5i

…he action's inputSchema before dispatch

The connector_action executor now judges the evaluated input against the
action's declared inputSchema (required, type with OpenAPI nullable,
properties, items) after the value slots resolve and before the handler
is called. A failing call fails the step with code VALIDATION_ERROR, names
every missing or mistyped parameter by its dotted path, and sends nothing.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgX3dtTQwah2MjCjf5i
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/connector-openapi, @objectstack/service-automation, touching 14 documentable anchor(s).

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json e1dde1a5c15566bcc24d6c641b53d8282fcf655b.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 12 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e1dde1a5c15566bcc24d6c641b53d8282fcf655b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from cf7f389fc9d5f8b1b2b78474b4058327ed5327f1 — the merge of head 615683feb3c9472259fdcc1dfefc73028d1b9d46 into base e1dde1a5c15566bcc24d6c641b53d8282fcf655b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin cf7f389fc9d5f8b1b2b78474b4058327ed5327f1 && git checkout cf7f389fc9d5f8b1b2b78474b4058327ed5327f1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e1dde1a5c15566bcc24d6c641b53d8282fcf655b 615683feb3c9472259fdcc1dfefc73028d1b9d46 && git checkout -B drift-repro e1dde1a5c15566bcc24d6c641b53d8282fcf655b && git merge --no-ff 615683feb3c9472259fdcc1dfefc73028d1b9d46

node scripts/docs-audit/affected-docs.mjs --json e1dde1a5c15566bcc24d6c641b53d8282fcf655b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e1dde1a5c15566bcc24d6c641b53d8282fcf655b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head 7c7eed0 · 2026-10-30T09:50Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7c7eed0d44f88f0df94655ba2a050864b2dac91d
Local-runs: none
Scope: packages/services/service-automation/src/builtin/connector-nodes.ts, packages/services/service-automation/src/builtin/connector-input-schema.test.ts, .changeset/27995-connector-action-input-schema.md + packages/connectors/connector-openapi/src/openapi-connector.ts (buildInputSchema, the per-action handler, interpolatePath, stringifyValues, buildUrl, applyAuth), packages/connectors/connector-mcp/src/mcp-connector.ts, packages/connectors/connector-slack/src/slack-connector.ts, packages/connectors/connector-rest/src/rest-connector.ts, packages/services/service-automation/src/builtin/value-slot-map.ts, packages/services/service-automation/src/engine.ts (NodeExecutionResult, getConnectorDescriptors, resolveConnectorActionEffect, the FLOW_INPUT_SCHEMA_INVALID retry short-circuit), packages/services/service-automation/src/connector-pull.ts, packages/spec/src/integration/connector.zod.ts (ConnectorActionSchema.inputSchema), skills/objectstack-automation/SKILL.md, skills/objectstack-automation/references/integration-recipes.md, content/docs/automation/connectors.mdx, card #27968

Reviewed at 2026-10-30T10:06Z, adversarially, on: the net diff origin/main...pr28035 (base f296334, 3 files, +378/-0), card #27995 (body; triage 6517799549; claim 6518217530 and amendment 6518274130; os-dev-report 6519820229), the PR body, file list and comments (Contract-review-claim 6519856849), and the head's check-runs. Read-only: nothing built, run or re-run.

① Derived judgments

  • Premise holds. On main the executor hands the evaluated connectorConfig.input to the handler with no comparison against the action's inputSchema; the openapi handler then interpolates a missing path value as the literal placeholder (interpolatePath leaves {petId} when the value is null or undefined), which is the card's measured /pets/%7BpetId%7D.
  • Placement. The check runs after resolveValueSlotMap (so envelopes are judged on their evaluated value — pinned) and before handler(); the root is always a plain object (resolveValueSlotMap returns {} for an absent map); on a refusal the handler is never called (pinned by the sent recorder in every refusal test). The schema is read from getConnectorDescriptors(), whose inputSchema: a.inputSchema is the stored def's value unchanged, so what is enforced is what GET /api/v1/automation/connectors shows.
  • Error contract: right code, right class, no leak. VALIDATION_ERROR is the first member of StandardErrorCode (ADR-0112 D2, the 400 bucket; the pull door in connector-pull.ts answers its own input faults with the same code), carried on NodeExecutionResult.code onto $error.code (pinned through a fault edge). errorClass is left at its runtime default, so a fault edge or try_catch routes it — correct, since the input can come from run data; a guard refusal would be unroutable. The message names the node, connectorId.actionId, each violation by dotted path with the declared type beside the kind received, caps at five then a count, states nothing was sent, and prescribes connectorConfig.input plus the connectors route (served by dispatcher-plugin.ts; route-ledger row GET /automation/connectors). Only schema keys, array indices and JSON kinds appear: no input value leaks. Consistent with the executor's other connector_action 'id': refusals.
  • The subset is safe on the producers' real shapes for everything it declines to judge: mcp passes tools/list schemas through verbatim and the walker skips $ref/$defs, anyOf/oneOf/allOf, enum, format, bounds, additionalProperties, boolean sub-schemas and tuple items, reading required only off the object it is on (no parent misread); 3.1 type lists and 3.0 nullable both admit null; integer is a whole number (bigint included); a Date/Map is unjudged; slack's literals and rest's request (no required, untyped body) behave as before. Array-of-object required is reached through items. Arrays and body wrapped under a key are judged where they sit.
  • False refusals found — the openapi provider's path/query/header sections are string-serialized and null-dropping (stringifyValues drops null/undefined and applies String(v); buildUrl drops null; interpolatePath applies String(value)), and the check judges them as JSON:
    1. MUST-FIX — null on an OPTIONAL property is refused ('query.tag' must be string, got null) although the provider drops it and the wire equals omission, which the schema permits. A CEL envelope over an empty record field ({"tag": rec.tag}) yields exactly this, and such a call succeeded before this head. Treat null/undefined on a property that is not in required as absent; keep judging null on a REQUIRED property (interpolatePath would send the literal placeholder — the card's bug), and keep nullable/type-list null as is.
    2. MUST-FIX (cited producer) — buildInputSchema demands parameters the connector's auth supplies. An Authorization or api-key header, or an api-key query paramName, declared as a required parameter in the document lands in that section's required, the section becomes root-required, and input: {} is refused with missing required 'header' (or 'query'). OAS 3.0.3 §4.7.12.1 says Accept/Content-Type/Authorization header parameters SHALL be ignored; applyAuth overwrites them after the input merge anyway, so the demanded value could never take effect; the only way through is to write the credential into connectorConfig.input, which the platform forbids. Harmless while the schema was advisory; this narrowing makes every call on such an action fail. Fix at the producer — buildInputSchema(op, auth) in openapi-connector.ts drops the three reserved header names and the header/param auth supplies (the auth is in scope in createOpenApiConnector). That is a connector-openapi edit: the claim's file surface said stop on breach and report, and the report named the shape (inventory item 1) without naming the breach. The seat widens the surface for it.
    3. SHOULD-FIX — the string spelling of an integer/number/boolean parameter in those sections (path.petId: '7', query.limit: '10') is refused although the provider sends String(value) and the wire is byte-identical to the number. A text field holding a vendor id feeding an integer path parameter is routine; the changeset declares this refusal as intended. Either coerce in the string-serialized sections (what OpenAPI request validators do) or make the CEL cast (int()) the prescription in the error text and the changeset's Fix line. Strict judging stays right for body and for mcp arguments, which travel as JSON.
  • False passes: none found. getPet {} and { path: {} } refuse; the path section is root-required whenever present; nested required is reached through properties and items; a required name absent from properties is still judged.
  • Inventory and battery versus the real schemas. The in-repo flows pass literal {} or strings to string parameters; the showcase openapi getHealth has no inputSchema and the mcp fixture requires only text. Neither the inventory nor the battery at this head exercises null on an optional parameter, a numeric string, or an auth-declared parameter — the inventory is honest about what exists in-repo and blind to 1–3 above.
  • Other doors. connector-pull.ts dispatches the same handlers (handler(input, …) built from connectorSource.input plus the watermark query) without this check — the same defect through the pull door, outside this card's title; it is a finding to file, not a fix here. No REST route and no MCP-server tool invokes a connector action directly; AI tools reach connectors through flows, so they are covered.
  • automation: registration does not judge a connector_action node's connectorConfig.input value slot, because the spec expression ledger cannot address a sibling block of config #27968 untouched. No registration-time judging was added; constants stay on that card's ledger path as triage ruled.

② Semver level

@objectstack/service-automation patch — correct. Metadata-protocol: none holds: registration and objectstack validate accept the same authorable text as before (the refusal is at dispatch; a registration-door narrowing would be narrowed, and that is #27968), so the level follows the kind, and the ! title, BREAKING banner, Clause-②: no (narrowing), Metadata-protocol: none and the single ADR-0087 marker not-required (no-migration-prescription) ride on patch (Post-Task Checklist step 3; a non-protocol breaking change ships patch and keeps banner and disposition). Both declaration lines appear in the PR body and the changeset body; the changeset carries FROM, TO, who sees it and the Fix. The fixed group versions the package with the rest. Check Changeset and the lint gates carrying the ADR-0087 and changeset checks are green on the head.

③ Boundary flags

  • Dev deviations, accepted: PR-body attribution through the thread harness (the seat may patch the body to the seat form); the subset checker inline in connector-nodes.ts rather than a new module (file surface).
  • Open questions: none declared; the three findings in ① are the ones this review owes back.
  • Out-of-scope findings: the dev's one (descriptor list rebuilt per dispatch; carrier none) — accepted; a resolveConnectorActionInputSchema beside resolveConnectorActionEffect reading the stored def is the right home. Missing: the pull-door sibling named in ① — PD chore: version packages #10 asks for every call site; the seat files it.
  • Retry. runtime makes the refusal routable, but under errorHandling.strategy: 'retry' a literal-only input re-derives a certainty 1+maxRetries times — the maintainer ruled the sibling FLOW_INPUT_SCHEMA_INVALID non-retryable for exactly that reason. Constants are automation: registration does not judge a connector_action node's connectorConfig.input value slot, because the spec expression ledger cannot address a sibling block of config #27968's path by triage; carry the note there, no change here.
  • Skills-docs: none — SHOULD-FIX. skills/objectstack-automation/SKILL.md line 88 and references/integration-recipes.md line 18 teach that connectorConfig.input "is sent as written"; this head falsifies it (an incomplete or mistyped input is not sent), and content/docs/automation/connectors.mdx's failure-modes table has no row for the new refusal. Rewrite the line to follow-up owed — FILES; the seat files the card and rewrites it to #N before ACCEPT.
  • Dogfood battery at the final head is on record in report 6519820229 (2026-10-30T09:48Z) and in the PR body: 357 files passed / 3 skipped, 2875 tests passed / 20 skipped, os-verify-lock command-exit 0.
  • Check-runs on the head, read at 2026-10-30T10:06Z: 52 runs, all completed — 37 success, 15 skipped (the UI and opt-in lanes, paths-filtered), 0 failed, 0 in progress. Green among them: Governed Surface Queue Guard, Build Core, Plan Test Core shards and Test Core 1–8, Type Check · workspace / source gates / debt ledger / consumer gates, every Lint lane, Check Changeset, Dogfood Regression Gate, Test UI, Live E2E UI. Their conclusions are the gate verdicts; this review adds none.

④ Design judgment

  • connector-nodes.ts — MUST-FIX ①1; SHOULD-FIX ①3; SHOULD-FIX read the schema from the stored def (as resolveConnectorActionEffect does) instead of rebuilding the descriptor list per dispatch; nit: when a required object section is absent, also name its own required children (path.petId) so the first refusal already carries the parameter. The hand-written subset over a shared validator is sound: ajv is objectql-private (a devDependency here), compiles $ref eagerly, rejects an unknown $schema, and does not know nullable, so it would mis-refuse the very shapes mcp and openapi emit; a dependency change is outside the claim. The no-new-dependency reasoning holds.
  • connector-input-schema.test.ts — SHOULD-FIX. The three schemas are hand-copied replicas, and the real-door probe (LiteKernel + ConnectorOpenApiPlugin) is uncommitted, so the getPet {} false pass is pinned against a replica, not against buildInputSchema's output. Pin it in connector-openapi's own suite (openapi-provider.test.ts already boots AutomationEngine) through createOpenApiConnector, and add preservation pins for null-on-optional and an auth-declared parameter once ①1–2 land. 11 tests / 7 red before / 7-leg ablation: meaningful — the single && false ablation reds exactly the seven refusal pins and leaves the four preservation pins green, as it should.
  • .changeset/27995-connector-action-input-schema.md — SHOULD-FIX after ①1–3: re-state the "Who sees it" example (the '7' case) and the Fix line (cast or coercion); declarations, marker and migration prose are otherwise complete.
  • openapi-connector.ts (cited) — MUST-FIX ①2.
  • mcp-connector.ts, slack-connector.ts, rest-connector.ts, value-slot-map.ts, engine.ts, connector.zod.ts (cited) — hold against the hunks' claims.
  • connector-pull.ts (cited) — file the sibling.

Implemented-by: claude/project-thread-9c0do1
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: FAIL

…prescribe the cast for text where a number is declared

Contract review round 1 on the connector_action inputSchema check:
null on an optional property no longer refuses (a provider that
serializes the section drops it); a refusal of text where an integer,
number or boolean is declared prescribes int()/double()/bool(); a missing
required section also names the parameters it requires.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgX3dtTQwah2MjCjf5i
…t of an action's inputSchema

The connector_action executor now refuses a call whose input breaks the
action's inputSchema, so buildInputSchema stops demanding the Accept,
Content-Type and Authorization header parameters (ignored per OAS 3.0.3)
and the header or query parameter the connector's api-key auth fills.
Pinned through a real boot on buildInputSchema's own output.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgX3dtTQwah2MjCjf5i
@github-actions github-actions Bot added size/l and removed size/m labels Oct 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head 615683f · 2026-10-30T11:23Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 615683feb3c9472259fdcc1dfefc73028d1b9d46
Local-runs: none
Scope: .changeset/27995-connector-action-input-schema.md, packages/connectors/connector-openapi/src/input-schema-dispatch.test.ts, packages/connectors/connector-openapi/src/openapi-connector.ts, packages/services/service-automation/src/builtin/connector-input-schema.test.ts, packages/services/service-automation/src/builtin/connector-nodes.ts + packages/connectors/connector-openapi/src/openapi-provider.ts, packages/connectors/connector-openapi/src/connector-openapi-plugin.ts, packages/spec/src/shared/connector-auth.zod.ts (ConnectorAPIKeySchema, RestAuth's source union), packages/services/service-automation/src/engine.ts (getConnectorDescriptors, celScope), packages/services/service-automation/src/builtin/value-slot-map.ts, packages/runtime/src/domains/automation.ts (GET /connectors), packages/formula/src/cel-engine.test.ts and cel-error-classification.test.ts (the dialect's int/double/bool), packages/services/service-automation/src/connector-pull.ts (#28040), connector-nodes.ts as #28037 left it on main

Delta review at 2026-10-30T11:36Z, adversarially, of head 615683f against the FAIL record 6520142501 (2026-10-30T10:07Z) on 7c7eed0. Read: the net diff origin/main...pr28035 (base now 847e424, 5 files, +602/-2) against the old net diff (3 files, +378/-0); the two non-merge commits since 7c7eed0 (b2149dc, 6785709); claim amendment 2 6520164540 (2026-10-30T10:08Z); os-dev-report 6521455883 (2026-10-30T11:22Z); the PR body, file list and comments (claim 6521477850 on this head); the head's check-runs. Read-only: nothing built, run or re-run; the one merge below is a server-free git merge-tree read. Every judgment of 6520142501 not named here stands unchanged at this head: premise, placement, error contract, the unjudged subset's safety on the three producers' shapes, no false passes, #27968 untouched, the pull-door sibling, the retry note.

① Derived judgments

  • The delta is the two patch commits and nothing else. git log --no-merges origin/main..head lists a9f9f28, c75e9aa (the old head's content) plus b2149dc (connector-nodes.ts, its test, the changeset) and 6785709 (openapi-connector.ts, the new openapi test, the changeset); neither touches a path outside the five. The merge 615683f equals its second parent e1dde1a (an ancestor of origin/main) outside the five files and its first parent 6785709 on them, so the raw 75-file old-to-new diff is main's motion, not the PR's. Main has since moved again: fix(service-automation): a dispatch to a held connector instance names its fix, not a retry #28037 (b59bf60) edited connector-nodes.ts after e1dde1a. GitHub reports the PR mergeable and clean; a read-only git merge-tree --write-tree origin/main head produces one file in which the automation: a held connector instance (a configuration fault or a refused stored credential) still tells a dispatch "the platform retries automatically", and the connectors page has no row for a configuration fault met on a retry #28016 held-instance refusal sits before handler resolution, input evaluation and this check, so the two refusals compose (a held instance refuses before its input is judged) and the queue leg has nothing to resolve by hand.
  • Must-fix ①1, null on an optional property — resolved, at every level. collectInputSchemaViolations now builds required as a set per object and, in the property loop, continues on record[key] === null when the key is not in it; the required loop still counts a missing key or undefined as missing. The recursion runs the same two loops at each object it reaches, so the rule holds for the root, each section, a nested object and an object inside items alike. undefined behaves as absent everywhere (skipped in the property loop, missing in the required loop). null on a REQUIRED property is still judged: it falls through to the type test and reads 'path.petId' must be integer, got null, with no cast hint (pinned). nullable: true and a type list holding null admit null as before. A null array ITEM is not a property and is still judged against items (refused unless nullable), which is right for a JSON body and for mcp arguments. Pinned: service-automation 14 tests (3 new: null-required refused; null-optional dispatches, on a non-nullable boolean; the int() cast dispatches), and the dev's ablation of the single continue reds exactly the null-optional pin (1 failed / 13 passed, blob restored); and in connector-openapi test 2, on buildInputSchema's real output over tag: { type: string } (not nullable), the call is sent and the recorded URL carries no tag.
  • Must-fix ①2, buildInputSchema(op, auth) — resolved, and it mirrors applyAuth branch for branch. A header parameter whose lower-cased name is accept, content-type or authorization is dropped (OAS 3.0.3 §4.7.12.1). The api-key header is dropped when auth.type === 'api-key' && !auth.paramName, compared lower-cased against headerName ?? 'X-API-Key', which is exactly applyAuth's else-arm and the spec default in ConnectorAPIKeySchema; the api-key query parameter is dropped by exact name when paramName is set, which is applyAuth's first arm (query names are case-sensitive, so exact is right). bearer and basic set only Authorization, covered by the ignored set; none sets nothing; oauth2 is outside RestAuth (the open-source subset) so applyAuth has no arm for it and nothing is supplied or dropped, consistent; ConnectorAPIKeySchema has no cookie location (only headerName/paramName), and a document's in: cookie parameter was already skipped. A section left with no properties is not emitted and so is not root-required (pinned 3a: the schema has no header key); a section left with only optional parameters loses sec.required and so its root-required entry. Call sites: git grep buildInputSchema at this head finds one, line 217 of createOpenApiConnector, where auth is already config.auth ?? { type: 'none' }; the declarative provider (openapi-provider.ts) passes ctx.auth as RestAuth into that same generator, so a declaratively registered connector gets the same schema. What is published: the def's inputSchema reaches designers only through registerConnector → getConnectorDescriptors() (engine.ts, inputSchema: a.inputSchema, unchanged) → GET /api/v1/automation/connectors; no MCP tool and no AI catalog reads a connector action's inputSchema (grep across packages), so the one published shape and the one enforced shape are the same object, pinned by 3a/3b reading the descriptor. Side effects: the handler, interpolatePath, stringifyValues, buildUrl and applyAuth are untouched; an input that still writes header.Accept or a non-credential header passes (additionalProperties is unjudged) and reaches the wire as before, Authorization then overwritten by applyAuth as before; Accept is not set by the transport, so only its advertisement is lost. Observation, acceptance-note grade: a programmatic defaultHeaders entry can also supply a document-required header and the schema still demands it; unlike the auth case the input's value takes effect (input headers override defaults), the declarative provider passes no defaultHeaders, and nothing in-repo passes one to this generator, so it is not a false refusal of the kind ①2 named.
  • Should-fix ①3, no coercion, cast prescription — accepted as implemented, and the text is accurate. Text is never converted; the refusal appends cast it in a CEL envelope with int(), double() or bool() only when a string met a declared integer, number or boolean (textForScalar), so a null-on-required or wrong-object refusal carries no misleading hint (pinned). The dialect is right: value envelopes go through evaluateValueEnvelope to the formula package's CEL engine (@marcbachmann/cel-js), whose int("3"), double("3.5") and bool("true") are pinned as stdlib in packages/formula/src/cel-engine.test.ts, with their conversion errors classified in cel-error-classification.test.ts. The changeset's Fix example int(record.vendor_id) names a real scope handle (record is the trigger record's alias of $record in celScope). The TO bullet ("Text is never converted"), the Who-sees-it line (the "7" case, "the error then says to cast it") and the Fix line (the envelope with int(), double(), bool()) agree with each other and with the code. The nit landed too: a missing required section names its required children (missing required 'path' (with 'path.petId')), pinned in both suites.
  • New tests. input-schema-dispatch.test.ts boots LiteKernel + AutomationServicePlugin + ConnectorOpenApiPlugin with a recording fetchImpl and reads the schema off the live descriptor, so every pin is on buildInputSchema's real output at the real door, which is what 6520142501 asked for. The fixture declares X-Api-Key and Authorization as required headers and api_key as a required query parameter, so with openapi-connector.ts at the previous commit every call without a header section was refused for missing required 'header': tests 2, 3a and 3b red, test 1 green because the executor's refusal already stood from a9f9f28 and nothing in the fixture can admit {}. Read precisely: 3a/3b red→green is direct evidence for the auth drop; test 2's red was the auth demand, not the null rule, whose evidence is the service-automation ablation and test 2's wire assertion; test 1 is the preservation pin of the card's own bug on the real producer, never expected to be red at this commit order. Meaningful as a set. Each test shuts its kernel down in finally.
  • engine.ts should-fix, left as an accepted deviation — the seat's call, and the right one. Amendment 2 declines to widen the card onto engine.ts; the per-dispatch cost is one map over registered connectors; a resolveConnectorActionInputSchema beside resolveConnectorActionEffect stays the right home for a later card. Recorded in the report's deviations.
  • Inventory at this head. The in-repo flows and dogfood fixtures pass what they passed before; none exercises an auth-declared parameter, so the openapi test is the only in-repo call exercising ①2, as the old record noted the battery was blind here. The battery's role is preservation, and it is on record at this head (③).

② Semver level

@objectstack/service-automation patch — unchanged, correct (6520142501 ②). @objectstack/connector-openapi patch — correct and required. The package is released (18.2.0, not private) and its src changed, so a changeset is mandatory and skip-changeset is not available. Metadata-protocol: none holds for it: no authorable key changes shape; what changes is a runtime-projected descriptor, from which three ignored header names and the auth-supplied parameter disappear. At the dispatch door this is a widening of what is accepted; at the descriptor it is a projection losing entries, not a spec key, so no retirement surface or tombstone applies. Clause-② is one line per changeset with at most one arm, and this single changeset's no (narrowing) names the dominant direction of the one change it describes; the connector-openapi paragraph states FROM and TO for its part, the OAS clause, the auth path the credential still takes, and the route that lists the action without them. Covered; a second arm is not writable and no (widening) is malformed. Level follows the kind: none ⇒ patch for both. The ADR-0087 marker stays single and still true. Check Changeset and the lint lanes carrying the ADR-0087 and changeset gates are green on this head.

③ Boundary flags

④ Design judgment

  • connector-nodes.ts — holds. The two loops over one required set make the null rule and the missing rule read from the same source; the hint flag rides the violation collector rather than re-parsing the message; the comment at the continue states the wire reasoning so the leniency is deliberate and findable. Still the right subset for schemas the platform does not own.
  • connector-input-schema.test.ts — holds. The three replicas remain, now as the harness for the executor's rules with the producer-level pins moved where they belong; the 14/3-new/1-of-14 ablation is meaningful.
  • openapi-connector.ts — holds against the ruling it implements and against applyAuth, its first consumer: the drop set is derived from the same auth object and the same default the request path uses, so the two cannot disagree without a shared edit. The OAS clause is cited at the constant.
  • input-schema-dispatch.test.ts — holds; the one real-door suite for this contract, read as in ①.
  • .changeset — holds; both packages, both declarations, one marker, FROM → TO, the cast prescription, the connector-openapi paragraph.
  • openapi-provider.ts, connector-openapi-plugin.ts, connector-auth.zod.ts, engine.ts, value-slot-map.ts, runtime automation.ts, the formula tests (cited) — hold against the hunks' claims.

Implemented-by: claude/project-thread-9c0do1
Reviewed-by: session_01L6bhgUCizoYWQVJWjhdR6F

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

  • 改了什么:flow 里的 connector_action 节点在派发前,用动作声明的 inputSchema 校验求值后的输入,只判 required、type(含 OpenAPI 的 nullable 和 integer)、properties、items。不通过时,这一步以 VALIDATION_ERROR 失败,逐个点名参数路径并给处方,请求不会发出。可选参数传 null 视为未传。字符串形式的数字或布尔值不做强转,处方里给出 CEL 转换 int() / double() / bool()。connector-openapi 生成的 inputSchema 不再要求三个保留头,也不再要求由 auth 提供的头或参数。
  • 为什么改:以前 inputSchema 只是提示,缺必填参数的调用照样发往远端,例如 getPet {} 会把占位符原样发成 /pets/%7BpetId%7D。
  • 风险与回滚:这是收窄。以前能发出去、但不符合声明的调用,现在会在本地失败,失败可以接 fault 边。示例和 dogfood 中没有调用因此变成失败。回滚 = revert 本 PR。
  • 先看:packages/services/service-automation/src/builtin/connector-nodes.ts,以及 connector-openapi 的 buildInputSchema。
  • 两分钟验证:pnpm --filter @objectstack/service-automation exec vitest run src/builtin/connector-input-schema.test.ts。
  • 需要你做的:无。由席位落地。

席位意见: ACCEPT。契约复核首轮 FAIL 指出的两条 must-fix 都已修好,增量复核 PASS;最终 head 上的 battery 全绿。

席位核对

ACCEPT · head 615683feb3c9472259fdcc1dfefc73028d1b9d46 · 2026-10-30T11:54Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

1 participant