You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 91fa06d
Browse filesBrowse the repository at this point in the historyBrowse files
docs(skills,automation): a connector_action input is judged against the action's inputSchema before dispatch, refused as VALIDATION_ERROR (#28170)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DBrWLqn9GU5duvKTbHrFr2M)_
domain:skills#2 · os-elon-musk · session_016WV4cwp1Wc6nB6ePmS4ePn
Fixes#28041
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:只改文档,不改代码。自动化 skill 的 `connector_action`
一行、`integration-recipes.md` 的输入条目,现在写明:求值后的 `connectorConfig.input`
在派发前按动作声明的 `inputSchema` 校验(`required`、`type` 含 OpenAPI
`nullable`、`properties`、`items`),不符即以 `VALIDATION_ERROR` 失败(可被 `fault`
边接住),请求不发出;可选参数上的 `null` 当作未传;文本里的数字 id 要在 CEL 里 `int()`
转型。`connectors.mdx` 的故障表加一行。
- 为什么改:PR #28035(#27995)合入后,旧文案仍教「输入原样发出」,契约复审 6520142501 点名此处欠文档。
- 风险与回滚:两个 skill 文件都在 token 天花板上(余量 0),新文字全靠同文件删重复付清,不动 ratchet
脚本:SKILL.md 6457→6452,recipes 2236→2236。回滚即 revert。
- 先看:`skills/objectstack-automation/references/integration-recipes.md` 的
Holds 第二条,再看 SKILL.md:88。
- 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期 exit 0。
- 需要你做的:欠一次契约复审(CONTRACT_REVIEW_TIER);落地档以登记册为准,本地读为 Tier
H(`skills/**`),需 os-zhuang 或 hotlong 批准。
## What changes
- **`skills/objectstack-automation/SKILL.md`, the `connector_action`
row:** the evaluated input is judged against the action's `inputSchema`
(`required`, `type`, `properties`, `items`) before dispatch; a breach
fails the step `VALIDATION_ERROR` (`fault`-routable) and nothing is
sent; `os validate` does not judge it yet (#27968).
- **`references/integration-recipes.md`, Holds bullet 2:** "any other
value sent as written" becomes "kept as written", then the same judgment
with OpenAPI `nullable`, null-on-optional read as absent, the per-path
naming, and the `int()` cast for a string-held id (`path: { dialect:
'cel', source: '{"petId": int(record.vendor_id)}' }`, the shape the
merged suite pins at `connector-input-schema.test.ts:208`).
- **`content/docs/automation/connectors.mdx`, Failure modes at a
glance:** one row for the refusal, with the code, the routing, the
remedy (`int()` / `double()` / `bool()`), the listing route and the `os
validate` note. The cross-domain exception is triage 6521031879.
Wording measured against the merged code (`ec7e15abc`,
`packages/services/service-automation/src/builtin/connector-nodes.ts:126-163`
and `collectInputSchemaViolations`), not the card's paraphrase. Shipped
message: `connector_action '<node>': input for '<connector>.<action>'
does not match the action's inputSchema — <violations>. Nothing was
sent. Fix connectorConfig.input so it satisfies the inputSchema GET
/api/v1/automation/connectors lists for this action.` plus, for text
where a number or boolean is declared, `A text value for an integer,
number or boolean parameter is not converted: cast it in a CEL envelope
with int(), double() or bool().` The result carries `code:
'VALIDATION_ERROR'` and the default `runtime` class, so a `fault` edge
receives it (pinned in the same suite). #27968 is still open, so the `os
validate` note stays.
## Paid for in the same file (restatement only, each with a surviving
home)
SKILL.md: the `approval` row's "(from `plugin-approvals`)" (the
`requires` table row says it), "never an `http` node holding a
credential" in § Integrations (the `http` row says it), the
filter-operand "(not `fault`-routable)" (the ROUTES table lists that
case as not routing), and "`notify` delivers, `connector_action` reaches
a vendor, a function computes" (the node rows and the PURE-compute
paragraph below say it). 6457 → 6452 tokens.
integration-recipes.md: recipe 1's "trimming still keeps the package
small" (recipe 1's comment says to trim), recipe 4's two comments "an
inbound post carries no trigger user" (recipe 1 teaches the same `runAs`
reason) and "never a literal secret" (the intro says the hook secret
goes to the deployment), recipe 5's "// inline OpenAPI 3.x" (the next
line spells `openapi: '3.0.3'` inline), the OAuth bullet's "Declare the
connector, fields and flows, mark the integration blocked …, and say so"
(the section heading says it), and "with its input schema, visible"
after recipe 5 (bullet 2 now carries the schema). 2236 → 2236 tokens,
headroom 0. No ceiling moved; `scripts/check-skills-token-ratchet.mjs`
is untouched.
## Verification
- `node scripts/check-skills-token-ratchet.mjs`: exit 0 (SKILL.md
6452/6457, integration-recipes.md 2236/2236). `pnpm
check:pm-skill-ratchet` exit 0.
- Gates: `dispatch-gates --commands` at the committed head `f2cd1d93`
derives 60 families; all 60 run in a fresh worktree after `pnpm install
--frozen-lockfile --ignore-scripts` and builds of `@objectstack/lint...`
and `@objectstack/client-react...`, exit codes recorded per command. 59
exit 0 (including `check:pm-skill-id-lint`, `check:skill-docs`,
`check:skill-examples`, `check:skill-refs`, `check:docs`,
`check:doc-anchors`, `check:doc-formula-expressions`,
`check:skill-identifier-liveness`). `check:dual-build-cjs-loads` exits 3
(needs every dist), NOT MEASURED. `--ran` reconciles 60 derived / 59 run
/ 1 NOT-MEASURED / 0 UNRUN; the derivation re-run at the final head is
identical.
- No changeset derived: no published package is touched, so
`skip-changeset`.
- Landing tier: `check-governed-merges.mjs --test` reads 2 of 3 paths on
the register (`skills/**`), Tier H. The register's reading at PR time is
the seat's.
- Not measured locally: repo-wide `pnpm lint`, the type-check lanes, and
the docs site build (CI's).
## Human Minimum
Each item is one rule from
[CONTRIBUTING.md › Human
Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum),
which links the AGENTS.md text that states it.
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(not touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; the derived lint families pass.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset. (no published
package: `skip-changeset`)
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (not touched)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block is the
one Chinese channel).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4cwp1Wc6nB6ePmS4ePn
---------
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: content/docs/automation/connectors.mdx
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -579,6 +579,7 @@ retry):
579
579
| MCP server unreachable / `tools/list` fails; remote spec URL unreachable or transiently failing (408/429/5xx) |**Not fatal**: the instance registers **degraded** (visible on `GET /api/v1/automation/connectors` with the reason), dispatches fail with that reason, and the platform retries on a 5s→5min backoff — recovery is automatic |
580
580
| A retry of a degraded instance reaches the upstream and meets a configuration fault (an `include` naming a tool or operation the upstream does not offer) |**Not fatal**: the instance stays **degraded**, now with the configuration reason, and is **not retried** — dispatches fail saying to edit its declaration; an edit of the entry (or a restart) re-attempts it |
| A `connector_action` input that, once evaluated, breaks the action's `inputSchema` (a missing `required` parameter, a wrong `type`) |**The step fails before dispatch** with `VALIDATION_ERROR`, naming each parameter path, and nothing is sent; a `fault` edge or `try_catch` can route it. Supply the parameter or send the declared type: text is never converted, so cast it in a CEL envelope (`int()`, `double()`, `bool()`). `GET /api/v1/automation/connectors` lists the schema; `os validate` does not judge the input against the `inputSchema` yet |
582
583
| A bad entry arrives via **runtime reload** (a Studio publish, a direct active save through `PUT /api/v1/meta/connector/:name`, or a delete, made on any replica — peers reconcile on [cluster lane 2](/docs/kernel/cluster#62-the-cluster-invalidation-family); or a dev reload) | Logged and **skipped**, never crashing the live server; a changed instance's old connector keeps serving until its replacement materializes |
583
584
584
585
## Catalog descriptors: entries without a `provider`
Copy file name to clipboardExpand all lines: skills/objectstack-automation/SKILL.md
+17-19Lines changed: 17 additions & 19 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -46,13 +46,12 @@ A **Flow** is a directed graph of nodes — the primary automation building bloc
46
46
|`autolaunched`| Runs without user interaction — triggered by events, APIs, or other flows |
47
47
|`screen`| Interactive — presents UI screens to the user (wizards, forms) |
48
48
|`schedule`| Runs on a cron/interval cadence declared on the **start node's `config.schedule`** — or a **per-record date sweep** via `config.timeRelative`, see *Time-relative triggers*|
49
-
|`record_change`| Fires automatically on record create/update/delete (bind via the `start` node's `triggerType`). `autolaunched` + the same `record-*` binding behaves identically; `record_change` also opts into the trigger-readiness lint |
49
+
|`record_change`| Fires automatically on record create/update/delete. `autolaunched` + the same `record-*` binding behaves identically; `record_change` also opts into the trigger-readiness lint |
50
50
|`api`| Inbound **webhook** — needs a start-node `secretRef` (or a literal `secret`; see *Inbound webhook triggers* below); a flow only ever started explicitly is `autolaunched`|
51
51
52
52
### Flow Node Types
53
53
54
-
Flows are built from **built-in node types** (plugins register more, e.g.
55
-
`approval`):
54
+
Flows are built from **built-in node types** (plugins register more):
56
55
57
56
#### Control Flow
58
57
@@ -85,15 +84,15 @@ Flows are built from **built-in node types** (plugins register more, e.g.
85
84
|:-----|:--------|
86
85
|`http`| Call an external HTTP API — always set `timeoutMs` (unset = no timeout). `url` and `headers` are served as written with the definition to every member who can read flows — only `signingSecret` and a start node's `secret` are withheld — so never a credential there: a credential means a declared connector called via `connector_action`|
87
86
|`notify`| Send a notification through the messaging service (inbox by default) |
88
-
|`connector_action`| Dispatch an action of a declared connector (§ *Integrations are declared, not coded*) or a plugin's. `connectorConfig.input` is a value-slot map like `subflow``input`; `os validate`does not judge it yet, so a malformed envelope fails at run time|
87
+
|`connector_action`| Dispatch an action of a declared connector (§ *Integrations are declared, not coded*) or a plugin's. `connectorConfig.input` is a value-slot map like `subflow``input`: `registerFlow` and `os validate`refuse a `{…}` token at `connectorConfig.input.<key>`. Once evaluated, it is judged against the action's `inputSchema` (`required`, `type`, `properties`, `items`) before dispatch: a breach fails the step `VALIDATION_ERROR` (`fault`-routable), nothing sent; `os validate` does not judge the `inputSchema` fit yet|
89
88
|`script`| Call a **registered** function named by `config.function` (see *Valid-but-silently-wrong*#3) |
90
89
| `screen` | Display a UI form to the user (screen flows only). `config.fields[]`: `{ name, label, type, required, options, defaultValue }`, `type` an open widget hint; `'file'` renders the upload widget and resumes with the `sys_file` id bound to `name` — a later `create_record` / `update_record` writes it into a `file` field, which claims the file: collect first, write after the last screen. `'record_link'` (never `'record'`, an object field type) takes no input — it shows the `reference` record whose id `defaultValue` evaluates to, by display name, as a link to its record page: the result screen of a flow that created records, `{ name: 'account_link', type: 'record_link', reference: 'crm_account', defaultValue: { dialect: 'cel', source: 'account.id' } }` over a `create_record` `outputVariable: 'account'`. Both keys required; `required` / `options` / `placeholder` / `min` / `max` refused; a resume value under its name refused (`400 INVALID_SCREEN_INPUT`). A completed `end` `message` is refused and a markdown link in a screen `description` renders literally. Next / Submit and "Step N of M" are not authored: the engine reads the graph when the screen pauses and serves `progress` (`nextScreen: 'none' | 'possible' | 'certain'`; `step` / `stepCount` only when every path agrees), which alone labels the button and draws the counter. A `decision`, `fault` edge, loop back, non-screen pause (`approval`, `wait`) or `subflow` after a screen answers `possible` — "Next", no count. `progress` in a node `config` is refused; never hand-roll a button label or step counter into `fields` |
91
90
92
91
#### Human Decision
93
92
94
93
| Node | Purpose |
95
94
|:-----|:--------|
96
-
|`approval`| Route a record for human sign-off — **suspends** the run until a decision, then continues down the `approve` / `reject` branch (from `plugin-approvals`) |
95
+
|`approval`| Route a record for human sign-off — **suspends** the run until a decision, then continues down the `approve` / `reject` branch |
Copy file name to clipboardExpand all lines: skills/objectstack-automation/evals/integrations-declared.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@
4
4
{
5
5
"id": 1,
6
6
"prompt": "Our contract-lifecycle app (objects `contract`, `crm_account`) needs DocuSign: send a contract out for signature, read an envelope's status, and reflect status changes on the record. DocuSign publishes a 4.7 MB Swagger 2.0 document with several hundred operations, authenticates with OAuth 2.0, and its Connect webhooks sign posts with their own HMAC header. Propose what to build and where.",
7
-
"expected_output": "A declarative instance inside the app, not a platform plugin and not a `connector-esign` family: `defineConnector` with `provider: 'openapi'`, whose `providerConfig.spec` is a packaged OpenAPI 3.x document converted offline from the Swagger 2.0 document and trimmed to the three operations (create envelope, get envelope, list status changes) (or `providerConfig.include` names those three operation keys, the `mcp` provider's allowlist meaning, where a non-array or unknown entry is fatal; trimming still keeps the package small). Fields on `contract` (`envelope_id`, `signature_status`). Flows dispatch `connector_action`; the callback is an `api` flow with a start-node `secretRef`. Two gaps are named plainly, each blocked on its platform card: the declarative `auth` union has no `oauth2`, so the integration is marked **blocked** on outbound OAuth (no short-lived token parked behind a `bearer` `credentialRef`); DocuSign Connect signs its own header, which the `api` door does not verify, so status is polled until vendor schemes are verified. `connectorConfig.input` is a value slot: the per-contract payload reads the record through a CEL envelope (`envelopeId: { dialect: 'cel', source: 'record.envelope_id' }`). The answer proposes no `registerConnector` code and no plugin.",
7
+
"expected_output": "A declarative instance inside the app, not a platform plugin and not a `connector-esign` family: `defineConnector` with `provider: 'openapi'`, whose `providerConfig.spec` is a packaged OpenAPI 3.x document converted offline from the Swagger 2.0 document and trimmed to the three operations (create envelope, get envelope, list status changes) (or `providerConfig.include` names those three operation keys, the `mcp` provider's allowlist meaning, where a non-array or unknown entry is fatal). Fields on `contract` (`envelope_id`, `signature_status`). Flows dispatch `connector_action`; the callback is an `api` flow with a start-node `secretRef`. Two gaps are named plainly, each blocked on its platform card: the declarative `auth` union has no `oauth2`, so the integration is marked **blocked** on outbound OAuth (no short-lived token parked behind a `bearer` `credentialRef`); DocuSign Connect signs its own header, which the `api` door does not verify, so status is polled until vendor schemes are verified. `connectorConfig.input` is a value slot: the per-contract payload reads the record through a CEL envelope (`envelopeId: { dialect: 'cel', source: 'record.envelope_id' }`), judged against the action's `inputSchema` before dispatch. The answer proposes no `registerConnector` code and no plugin.",
0 commit comments