From 1f28d891b0dea2eacccd8c98d70401c92c4c42a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 07:28:28 +0000 Subject: [PATCH] docs(skills): teach connector_action input as an evaluated value slot and the openapi include allowlist The automation skill still taught that `connectorConfig.input` is sent as written and that the `openapi` provider has no `include` key. Both stopped being true: the executor evaluates the input map through the shared value-slot map, and `providerConfig.include` is the mcp-style allowlist. Re-teach the SKILL.md row, the integration-recipes Holds bullet and recipe 1, and the integrations-declared eval case 1. Name that `os validate` does not judge the input slot yet. Pay the recipes growth with three same-file restatement cuts and raise its ratchet row by the measured remainder. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_016WV4cwkH2Pj7G4DWB8WTaf --- .../27982-automation-connector-input-slot.md | 12 +++++++++++ scripts/check-skills-token-ratchet.mjs | 20 ++++++++++++++++++- skills/objectstack-automation/SKILL.md | 2 +- .../evals/integrations-declared.json | 2 +- .../references/integration-recipes.md | 17 +++++++++------- 5 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 .changeset/27982-automation-connector-input-slot.md diff --git a/.changeset/27982-automation-connector-input-slot.md b/.changeset/27982-automation-connector-input-slot.md new file mode 100644 index 00000000000..a26b99e6949 --- /dev/null +++ b/.changeset/27982-automation-connector-input-slot.md @@ -0,0 +1,12 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): the automation skill teaches `connector_action`'s `connectorConfig.input` as an evaluated value slot, and the `openapi` provider's `providerConfig.include` + +Clause-②: no +Metadata-protocol: none + +- **`connectorConfig.input` is a value-slot map**, like `subflow` `input`: a top-level CEL envelope is evaluated in the run's scope, so a per-record payload (`{ dialect: 'cel', source: 'record.stripe_id' }`) is a declarative dispatch now. Any other value is sent as written; a `{…}` or `{{ }}` in it is literal text. The skill says plainly that `os validate` does not judge this slot yet, so a malformed envelope fails the step at run time. +- **`providerConfig.include` exists on the `openapi` provider**, with the `mcp` provider's allowlist meaning: absent exposes every operation, and a non-array or an unknown entry is a fatal configuration fault. Trimming the document is still taught, as the way to keep the package small, not as the only allowlist. +- The `integrations-declared` eval's first case follows: it no longer grades either stale limit. diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 289e5263252..450f26c3926 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -868,7 +868,25 @@ export const CEILINGS = new Map([ // request body or response, so the derived schema silently loses it): // 8705 -> 8814 bytes, 2177 -> 2204. // ceil($(wc -c < skills/objectstack-automation/references/integration-recipes.md) / 4) == ceil(8814 / 4) - ['skills/objectstack-automation/references/integration-recipes.md', 2204], + // 8814 -> 8942 (card #27982): the "Holds on every recipe" input bullet + // re-teaches `connectorConfig.input` as an evaluated value-slot map, with a + // record-field example and the not-yet-judged-at-`os validate` note, and + // recipe 1's closing sentence teaches `providerConfig.include` (the `mcp` + // allowlist meaning, a non-array or unknown entry fatal) instead of denying + // the key. Paid down first, three restatements each with a surviving home: + // recipe 1's comment "3.x only, … every `$ref` dereferenced offline" (the + // same comment says "OpenAPI 3.x document"; the paragraph under the recipe + // says to dereference every `$ref` offline), recipe 5's comment ", written + // from the vendor's docs" (its heading says write the document by hand), + // and the OAuth bullet's "four variants and" (the Holds `auth` bullet lists + // them). Arithmetic: 8814 -> 8942 (+128 net) -> ceil(8942 / 4) = 2236; + // headroom was 0 tokens, so the ceiling moves +32, pinned AT the + // measurement. Maintainer ruling 2026-10-27 on the seat's decision card + // 「skills 修复卡的教学和 eval 都撞 token 天花板,怎么付?」, tap A verbatim + // and untranslated: 「A:先削重复,余额按实测提高」 — the standing path of + // ruling record 6513306068 on #27847. + // ceil($(wc -c < skills/objectstack-automation/references/integration-recipes.md) / 4) == ceil(8942 / 4) + ['skills/objectstack-automation/references/integration-recipes.md', 2236], // 5569 -> 5587 (card #26298, audit #26184 row A2): the approval config table // gains `excludeSubmitter` (refused beside `onEmptyApprovers: 'auto_approve'`), // +105 tokens. Paid down first: Best Practices 1 and 4 restated the Branching diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index f62b522677f..58280edfa64 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -85,7 +85,7 @@ Flows are built from **built-in node types** (plugins register more, e.g. |:-----|:--------| | `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` | | `notify` | Send a notification through the messaging service (inbox by default) | -| `connector_action` | Dispatch an action of a declared connector (a `connectors:` entry with a `provider`, § *Integrations are declared, not coded*) or a plugin's. `connectorConfig.input` is sent as written — no CEL or `{{ }}` in it | +| `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 | | `script` | Call a **registered** function named by `config.function` (see *Valid-but-silently-wrong* #3) | | `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` | diff --git a/skills/objectstack-automation/evals/integrations-declared.json b/skills/objectstack-automation/evals/integrations-declared.json index 3ced2195463..a575bd17b2f 100644 --- a/skills/objectstack-automation/evals/integrations-declared.json +++ b/skills/objectstack-automation/evals/integrations-declared.json @@ -4,7 +4,7 @@ { "id": 1, "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.", - "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 trimmed, packaged OpenAPI 3.x document carrying only the three operations (create envelope, get envelope, list status changes), converted offline from the Swagger 2.0 document — the `openapi` provider has no `include` key, so the trimmed document is the allowlist. Fields on `contract` (`envelope_id`, `signature_status`). Flows dispatch `connector_action`; the callback is an `api` flow with a start-node `secretRef`. Three 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 sent as written, so a per-contract payload waits on the platform too. The answer proposes no `registerConnector` code and no plugin.", + "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.", "files": [], "assertions": { "must_contain": ["defineConnector", "provider: 'openapi'", "credentialRef", "connector_action", "type: 'api'", "secretRef", "Swagger 2.0", "blocked"], diff --git a/skills/objectstack-automation/references/integration-recipes.md b/skills/objectstack-automation/references/integration-recipes.md index 63f72b9a1cf..2b139946076 100644 --- a/skills/objectstack-automation/references/integration-recipes.md +++ b/skills/objectstack-automation/references/integration-recipes.md @@ -15,9 +15,10 @@ deployment and to the sender. Scaffolded apps list the three executors in - Actions come from the upstream (an OpenAPI `operationId`, an MCP tool name, the `rest` provider's single `request`) — authored on the entry, they are refused. -- `connectorConfig.input` is sent **as written** — no CEL envelope, no `{{ }}` - hole — so a per-record payload (an id from `record.*`) is not a declarative - dispatch today: a gap to name, not a plugin to write. +- `connectorConfig.input` is a value-slot map like `subflow` `input`: a top-level + CEL envelope is evaluated, any other value sent as written (`{…}`, `{{ }}` are + literal text) — `input: { customer: { dialect: 'cel', source: 'record.stripe_id' } }`. + `os validate` does not judge it yet, so a malformed envelope fails at run time. - The answer lands as `.` (`rest` / `openapi`: `{ status, ok, body }`; `mcp`: `{ ok, content, structuredContent? }`), read from a later node's value slot: `{ dialect: 'cel', source: 'bal.body.available[0].amount' }`. @@ -31,7 +32,7 @@ deployment and to the sender. Scaffolded apps list the three executors in ```ts // Trim the vendor's OpenAPI 3.x document to the operations the app calls and ship -// it in the package: 3.x only, package-relative, every `$ref` dereferenced offline. +// it in the package, package-relative. export const StripeConnector = defineConnector({ name: 'stripe', label: 'Stripe', type: 'api', provider: 'openapi', providerConfig: { spec: './src/connectors/stripe.trimmed.json' }, // holds `GetBalance` alone @@ -60,7 +61,9 @@ export const StripeBalanceSweep = defineFlow({ `requires: ['automation', 'triggers', 'job']`. Convert Swagger 2.0 or YAML and dereference every `$ref` offline, in-document ones included: the loader skips a `$ref` parameter, request body or response, and the derived input/output schema -silently loses it. The `openapi` provider has no `include` key — the trimmed document is the allowlist. +silently loses it. `providerConfig.include` allowlists operation keys as the +`mcp` provider's does (absent = every operation; a non-array or unknown entry is +fatal); trimming still keeps the package small. ## 2 · Remote MCP server, static header — GitHub @@ -124,7 +127,7 @@ name the gap and poll its status (recipe 1's shape) until vendor schemes verify. ```ts export const LegacyErp = defineConnector({ name: 'legacy_erp', label: 'Legacy ERP', type: 'api', provider: 'openapi', - providerConfig: { spec: { // inline OpenAPI 3.x, written from the vendor's docs + providerConfig: { spec: { // inline OpenAPI 3.x openapi: '3.0.3', info: { title: 'Legacy ERP', version: '1' }, servers: [{ url: 'https://erp.example.com' }], paths: { '/v2/open-orders': { get: { operationId: 'listOpenOrders', @@ -141,7 +144,7 @@ palette and to agents; `provider: 'rest'` instead when the path is per call. ## Not yet — declare the rest, mark the integration blocked, write no plugin - **OAuth-protected vendor** (DocuSign, Salesforce, Google): the declarative `auth` - union has four variants and no `oauth2`; the authorization-code / refresh + union has no `oauth2`; the authorization-code / refresh lifecycle is not delivered in the open tier. Do not park a short-lived access token behind a `bearer` ref. Declare the connector, fields and flows, mark the integration **blocked** on the platform's outbound-OAuth card, and say so.