Skip to content

Commit eac538c

Browse files
docs(skills): objectstack-automation calls the api flow secret required and routes explicit-only starts to autolaunched (#20796)
Fixes #20569 Clause-②: no `skills/objectstack-automation/SKILL.md` still described the `api` flow kind and its inbound-hook `secret` as they were before PR #20551: the Flow Types row said a `type: 'api'` flow is "invoked explicitly via the API / `engine.execute()`, **or** bound as an inbound webhook", and the `secret` row called the HMAC secret "strongly recommended — without it unsigned posts are accepted and a warning is logged". Both are false on `main`. Tier H (`skills/**`): a draft PR for the maintainer's hand — no seat readies, queues or arms it. ## What changed — one file, `skills/objectstack-automation/SKILL.md` (+5 / −6) | Line on `main` `7a09eee1` | Was | Now | |---|---|---| | `:51` Flow Types `api` row | explicit invocation **or** inbound webhook, route inline | "Inbound **webhook** — every `api` flow is bound to its hook endpoint and needs a start-node `secret` (see *Inbound webhook triggers* below); a flow only ever started explicitly is `autolaunched`" | | `:342` | "An `api` flow **can be** bound to an inbound HTTP endpoint" | "**is** bound" — every `api` flow is | | `:344-345` | "(the start `config` is a free-form record, so these keys are read at runtime, not Zod-validated)" | "(the start `config` is a free-form record with no Zod shape)" — `secret` is now judged before runtime, so only the still-true half stays | | `:350` `secret` row | "Strongly recommended — without it unsigned posts are accepted and a warning is logged" | "**Required** — without a non-blank one the flow is refused at registration (`os validate` too) and never armed at boot; the signature goes in `x-objectstack-signature`" | | `:352` Signature bullet | "sender sends `x-objectstack-signature: sha256=…`" | the value shape only (`sha256=` plus the hex) — the header name now lives in the `secret` row | Untouched on purpose: the `http` row (`:87`) and the examples-flows Slack node (PR #20778's same-day churn on this file). Kept abstract under the security disclosure rule: no request recipe was added; the bullet lost text. ## Measured at the code (`origin/main` `7a09eee1`) Every claim in the new text was read from the runtime, none recalled: - **The header.** `packages/triggers/trigger-api/src/plugin.ts:89` reads `c.req.header('x-objectstack-signature')`; `api-trigger.ts:78-81` (`verifySignature`) compares it, constant-time, with `sha256=` plus the hex HMAC-SHA256 of the raw body under the start-node secret. - **`api` is the inbound-webhook kind; there is no explicit-only `api` form.** `packages/spec/src/automation/flow-trigger-kind.ts:83`: `if (f.type === 'api' || triggerType === 'api') return 'api'`, and `AutomationEngine.deriveTriggerBinding` (`packages/services/service-automation/src/engine.ts:3526`) binds from that resolver, so every `type: 'api'` flow is handed to `trigger-api`. A `type: 'autolaunched'` flow with no start-node binding resolves to no kind (`:84`) — the explicit-only form. The enum is `['autolaunched', 'record_change', 'schedule', 'screen', 'api']` (`packages/spec/src/automation/flow.zod.ts:1060`). - **Refused at registration, never armed at boot.** `engine.ts:4311` calls `validateApiTriggerSecret` (`:10036-10052`), which throws on an `api` binding whose start node has no non-blank `config.secret`; every `registerFlow` call site try/catches per flow, so at boot the flow is skipped loudly. `trigger-api`'s own `start()` refuses the same binding before anything is stored or subscribed (`api-trigger.ts:133-143`: "not armed"). - **Every authoring door.** The `/automation` write doors answer the throw with `VALIDATION_FAILED_STATUS` (`packages/runtime/src/domains/automation.ts:2174-2178`). `os validate` runs `runAuthoringRules('validate', …)` (`packages/cli/src/commands/validate.ts:461`) over `AUTHORING_RULES`, which carries `validateFlowApiTriggerSecret` (`packages/lint/src/authoring-rules.ts:1170-1195`: `tier: 'gating'`, `commands: ALL`) and answers `flow-api-trigger-secret-missing` at `error` (`validate-flow-trigger-readiness.ts:875-895`). `/meta` runs the same table and throws its 422 `INVALID_METADATA` (`packages/metadata-protocol/src/protocol.ts:4933-4941`). - **Exposure.** `packages/services/service-automation/src/flow-credential-projection.ts:129` withholds the start node's `secret` from every served definition; the skill already says so at `:87`, so the row does not repeat it. ## `skills/**` readings (token = ceil(utf8 bytes / 4), the ratchet's own convention) | Reading | Before (`7a09eee1`) | After (`32847a29`) | Delta | |---|---|---|---| | `skills/objectstack-automation/SKILL.md` | 439 lines · 23106 bytes · 5777 tokens | 438 lines · 23125 bytes · 5782 tokens | −1 line · +19 bytes · +5 tokens (ceiling 5785, headroom 3) | | Whole package — the 54 hand-authored files the ratchet covers | 12786 lines · 143329 tokens | 12785 lines · 143334 tokens | −1 line · +5 tokens | | Whole package — every `SKILL.md` | 4395 lines | 4394 lines | −1 line | Line budget (PM-set: net +2 at most across the package): net **−1**. No ceiling raised and none lowered (the file grew by 5 tokens). No re-wrap: the one removed line is the `:345` clause replaced by its true half. The new text is paid by three in-file deletions, each of which keeps its home in this same file — the hook route is now stated once (`:343`, in the section the `api` row points to); "sender sends `x-objectstack-signature: `" folded into the `secret` row; "so these keys are read at runtime, not Zod-validated" cut to "with no Zod shape". Nothing left the published package. ## Verification Gates derived from the change with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` at head `32847a29` (no paths: the change set read from the merge base) and reconciled with `--ran`: **24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN**, every recorded exit code 0. The first run of `pnpm --filter @objectstack/lint run check:doc-formula-expressions` answered exit 3 (PREREQUISITE NOT MET: `@objectstack/formula` and `@objectstack/lint` unbuilt); it was re-run after `pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under the verify lock (`VERDICT command-exit 0`) and answered exit 0 — the exit-3 run is a non-measurement, not a red. Also green outside the derivation: `pnpm --filter @objectstack/spec run check:skill-refs`. `check:skills-token-ratchet` prints "skills/objectstack-automation/SKILL.md is 5782 tokens (ceiling 5785; headroom 3)". `check:skill-examples` is not owed: no edited block carries an `os:check` marker. No control bytes in the file. No package test or typecheck owed: the diff touches no package. ## Acceptance notes Census of `skills/**` and `content/docs/**` for sentences describing the `api` trigger or its secret (case-insensitive grep over `unsigned post`, `strongly recommended`, `invoked explicitly`, `explicit-only`, `hooks/:flowName`, `x-objectstack-signature`, `inbound webhook`, `type: 'api'`, `api … secret`, `autolaunched`; `content/docs/references/` and `content/docs/releases/` excluded as generated / release-owned): - `skills/**` outside this file: no sentence about the `api` flow trigger or its secret. The `type: 'api'` hits in `skills/objectstack-ui/rules/actions.md:22,38` and `skills/objectstack-ai/SKILL.md:158` are the UI action kind, not the flow kind. `skills/objectstack-automation/references/*` and `evals/*` carry no `api`-flow or `secret` sentence. - `content/docs/**`: no sentence calls the secret optional or `type: 'api'` explicit-only, so nothing there is false in the card's sense. Two observations, noted and not filed: `content/docs/automation/flows.mdx:92` (`api` — "Exposed as an API endpoint" / "HTTP request") is true but names neither the hook nor the secret; `content/docs/automation/webhooks.mdx:733-736` ("Inbound webhooks … reintroduce it only alongside a real inbound runtime") is the outbound protocol's non-goals list written before `trigger-api` (ADR-0041 Tier 1) and reads as if no inbound runtime existed — stale, not false; carrier: none. `content/docs/releases/v17/17-5.mdx` already states the requirement correctly. ## 维护者速读(草稿) **改了什么** — `skills/objectstack-automation/SKILL.md` 里两处过时说法:Flow Types 表的 `api` 行不再说 `type: 'api'` 可以「只显式调用」,改为「入站 webhook,每个 `api` 流都绑到它的 hook 端点、都要 start 节点的 `secret`;只显式启动的流是 `autolaunched`」;`secret` 行由「强烈建议」改为「必填」,写明缺失时的真实后果(注册时拒绝、启动时不装载),并点名签名头 `x-objectstack-signature`。另外三处小改是为 token 上限付账:hook 路由只在下方章节写一次、签名要点只留值的形状、「运行时才读取、不经 Zod 校验」只保留仍成立的后半句。 **为什么改** — PR #20551 之后,运行时所有写入口(`/automation` 写门、`os validate`、`/meta`、引擎注册、`trigger-api` 装载)都拒绝没有非空 `config.secret` 的 `api` 流;技能包却仍在教 AI 写一个运行时必拒收的流,并暗示 `type: 'api'` 有「只显式调用」的形态。每条新句子都从代码读出,正文附行号。 **风险与代价(含回滚)** — 纯文档改动,不发布任何包(`skills/**` 不在任何包的 `files[]` 内,打 `skip-changeset`)。token 上限 5785 内(现 5782,余量 3),行数净 −1,上限未动。风险仅限措辞;回滚为 `git revert` 单个 commit,无连带。 **席位意见** — (留空) **你要做的** — 以维护者身份审阅并合并这个 draft PR(Tier H:席位不得 ready / queue / auto-merge)。若想保留「read at runtime, not Zod-validated」原句,需另删等量内容守住 token 上限,请在评审中指出。 --- _Generated by [Claude Code](https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 157baa7 commit eac538c

1 file changed

Lines changed: 5 additions & 6 deletions

File tree

‎skills/objectstack-automation/SKILL.md‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ A **Flow** is a directed graph of nodes — the primary automation building bloc
4848
| `screen` | Interactive — presents UI screens to the user (wizards, forms) |
4949
| `schedule` | Runs on a cron/interval cadence declared on the **start node's `config.schedule`** (daily cleanup, weekly reports) — or a **per-record date sweep** via `config.timeRelative`, see *Time-relative triggers* |
5050
| `record_change` | Fires automatically on record create/update/delete (bind via the `start` node's `triggerType`). `autolaunched` + the same `record-*` binding behaves identically — the engine reads the start node either way; `record_change` also opts into the trigger-readiness lint |
51-
| `api` | Invoked explicitly via the API / `engine.execute()`, **or** bound as an inbound **webhook**: `POST /api/v1/automation/hooks/:flowName/:hookId` (see *Inbound webhook triggers* below) |
51+
| `api` | Inbound **webhook** — every `api` flow is bound to its hook endpoint and needs a start-node `secret` (see *Inbound webhook triggers* below); a flow only ever started explicitly is `autolaunched` |
5252

5353
### Flow Node Types
5454

@@ -339,17 +339,16 @@ defineStack({
339339

340340
### Inbound webhook (`api`) triggers (ADR-0041 Tier 1)
341341

342-
An `api` flow can be bound to an inbound HTTP endpoint:
342+
An `api` flow is bound to an inbound HTTP endpoint:
343343
`POST /api/v1/automation/hooks/:flowName/:hookId`. Configure it on the **start
344-
node `config`** (the start `config` is a free-form record, so these keys are
345-
read at runtime, not Zod-validated):
344+
node `config`** (the start `config` is a free-form record with no Zod shape):
346345

347346
| `config` key | Purpose |
348347
|:-------------|:--------|
349348
| `hookId` | URL path token (default `'default'`). **Rotate it to revoke** a leaked endpoint |
350-
| `secret` | HMAC-SHA256 shared secret. Strongly recommended — without it unsigned posts are accepted and a warning is logged |
349+
| `secret` | HMAC-SHA256 shared secret. **Required** — without a non-blank one the flow is refused at registration (`os validate` too) and never armed at boot; the signature goes in `x-objectstack-signature` |
351350

352-
- **Signature:** sender sends `x-objectstack-signature: sha256=<hex>` (GitHub/Stripe style).
351+
- **Signature:** `sha256=<hex>` (GitHub/Stripe style).
353352
- **Idempotency:** `x-idempotency-key` dedupes retries — author the flow to be idempotent (delivery is at-least-once).
354353
- **Queue-backed:** the endpoint ACKs `202` and enqueues; the flow runs on the consumer, never in-band. Requires the `queue` service (see prerequisite).
355354
- The JSON body surfaces to the flow as the trigger record (`record.*` / bare fields) plus `params`.

0 commit comments

Comments
 (0)