Skip to content

docs(skills): objectstack-automation calls the api flow secret required and routes explicit-only starts to autolaunched - #20796

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-20569-automation-skill-api-secret
Sep 30, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-20569-automation-skill-api-secret

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

…ed and routes explicit-only starts to autolaunched

The `api` Flow Types row said a `type: 'api'` flow could be invoked
explicitly OR bound as an inbound webhook; the engine binds every
`api`-kind flow to the inbound trigger, so the explicit-only form is
`autolaunched`. The `secret` row called the HMAC secret "strongly
recommended"; the runtime refuses an `api` flow with no non-blank
`config.secret` at registration (`/automation` doors, `os validate`,
`/meta`) and `trigger-api` never arms it. The row now says so and names
the header the signature goes in, read from `trigger-api`'s handler.

Paid in-file: the hook route stays stated once (the section the row
points to), the signature bullet keeps only the value shape, and the
"read at runtime, not Zod-validated" clause — now false for `secret` —
keeps only its true half.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Sep 30, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 32847a291404fbe17511948eff46011083008f10
Local-runs: none

Inputs: card #20569 (body + its three comments, the dev report 5905964684 included), PR #20796 (body, file list, the net diff against main at the head), and the check-runs on the head. Repository files read with git show / git grep at the head and at origin/main 8acdae9d. Merge-base is 7a09eee1; no main commit touched skills/objectstack-automation/SKILL.md between the two, so the net diff is exactly the one file, +5 / −6. Head repo is the base repo (not a fork); the PR is draft with auto-merge unset.

① Derived judgments

Accept-set and public-surface changes implied by the diff: none. One published skill file edited; no spec key, export, runtime behaviour, route or generated artifact moves. Every statement the changed lines make, judged at the code on main 8acdae9d:

  1. :51 "Inbound webhook — every api flow is bound to its hook endpoint" — right. packages/spec/src/automation/flow-trigger-kind.ts:83 answers api for type: 'api' (or a start-node triggerType: 'api'); AutomationEngine.deriveTriggerBinding (engine.ts:3478) takes that answer and activation hands the binding to trigger-api, whose route is HOOKS_PATH (packages/triggers/trigger-api/src/plugin.ts:25). One precision caveat, not a defect: the resolver ranks record-* / timeRelative / schedule ahead of api (:76-83), so a type: 'api' flow whose start node ALSO carries one of those binds to that trigger and is never asked for a secret — the lint header at validate-flow-trigger-readiness.ts:813-821 documents exactly this. That is a mixed declaration the section never teaches; the pre-PR sentence carried the same simplification and the record_change row already tells the author the engine reads the start node. Right as an authoring rule.
  2. :51 "needs a start-node secret" — right. engine.ts:10127-10142 throws on an api binding whose start node has no non-blank config.secret; api-trigger.ts:133-143 refuses the same binding independently.
  3. :51 "a flow only ever started explicitly is autolaunched" — right. The resolver returns no kind for an autolaunched flow with no binding (flow-trigger-kind.ts:84, header :55-57); the engine's own refusal sentence and the lint hint (validate-flow-trigger-readiness.ts:889-892) name the same form.
  4. :342 "An api flow is bound to an inbound HTTP endpoint" + the route — right (plugin.ts:25; the caveat in item 1 applies identically).
  5. :344 "the start config is a free-form record with no Zod shape" — right. flow.zod.ts:591 declares config: z.record(z.string(), z.unknown()).optional(); the FlowNodeSchema transform parses only an end node's config (:544-552); no start-node config schema exists under packages/spec/src/automation/.
  6. :349 "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" — right, on every door. Registration: registerFlow calls validateApiTriggerSecret at engine.ts:4311. Boot: every registerFlow call site catches per flow and warns (service-automation/src/plugin.ts:992-997, :2044-2048, :2092-2097), so the flow is skipped, never bound; ApiTrigger.start() additionally refuses before anything is stored (api-trigger.ts:138-143, "not armed"). /automation write doors: packages/runtime/src/domains/automation.ts:2180-2186 answers the throw with VALIDATION_FAILED_STATUS (400). os validate: packages/cli/src/commands/validate.ts:461 runs runAuthoringRules('validate', …) over AUTHORING_RULES, whose row validateFlowApiTriggerSecret (packages/lint/src/authoring-rules.ts:1184-1197) is tier: 'gating', commands: ALL, surfaces: CLI_AND_RUNTIME, runtimeTypes: ['flow'], and answers flow-api-trigger-secret-missing at error (validate-flow-trigger-readiness.ts:257, :857-895). /meta: packages/metadata-protocol/src/protocol.ts:4933-4941 runs the same table and throws 422 INVALID_METADATA. Header: plugin.ts:89 reads c.req.header('x-objectstack-signature').
  7. :351 "Signature: sha256= plus the hex (GitHub/Stripe style)" — right. api-trigger.ts:80 computes 'sha256=' + hex HMAC-SHA256 of the raw body; ADR-0041 docs/adr/0041-flow-trigger-family.md:119 says "per-flow secret; HMAC signature verification (GitHub/Stripe style)".

Deleted clauses, each judged for a surviving home:

  • "Invoked explicitly via the API / engine.execute(), or" — the false half the card names (item 2); not a rule to rehome. That any flow may be started explicitly stays at :47 (autolaunched … "triggered by events, APIs, or other flows"). Not a lost rule.
  • The inline route in the api row — home at :343, in the section the row points to. Not lost.
  • "can be bound" → "is bound" — the card's direction, right per item 1.
  • "so these keys are read at runtime, not Zod-validated" → "with no Zod shape" — the "read at runtime" half is now false for secret (judged at registration and by the lint rule); the "not Zod-validated" half survives in meaning (item 5). Not a lost rule.
  • "sender sends x-objectstack-signature:" — the header name rehomed into the secret row (:349); the inbound direction is carried by the section title and the route above it. Not lost.

Contradictions: none found. In-skill: :47, :326 (triggers token turns on api start bindings), :328 (queue absent → 503) and :87 (a start node's secret withheld from served definitions; flow-credential-projection.ts:10-12) all agree with the new text. references/* and evals/* at the head carry no sentence on the api flow or its secret (the autolaunched hits are examples consistent with :51). content/docs/automation/** at main: flows.mdx:88 (autolaunched "Invoked by other flows or API"), :92 (api "Exposed as an API endpoint / HTTP request" — true, incomplete), :1847, :2277 are all consistent; webhooks.mdx:733-736 (the outbound protocol's v1 non-goals note) predates trigger-api and is stale on main before this PR, neither introduced nor worsened by it. Frontmatter version: "1.3" untouched — the precedent on this file (7a09eee1, PR #20778) bumped nothing on a body fix.

Security disclosure constraint: right. The head states the route (once), the header name, the value shape and "GitHub/Stripe style"; main before this PR stated all four plus "sender sends". Nothing new; main's own lint hint (validate-flow-trigger-readiness.ts:889-891) already discloses more than the skill does.

Token ratchet, measured at both trees: 23106 → 23125 bytes, 5777 → 5782 tokens (ceil(bytes/4)) against the ceiling 5785 at scripts/check-skills-token-ratchet.mjs:323 — headroom 3; 439 → 438 lines. Matches the dev's readings byte for byte. The three in-file deletions that paid for it are the ones judged above.

② Semver level

skip-changeset with Clause-②: no — right. skills/** is in no released package's files[] (at main, only spec/lint gate scripts read that tree); create-objectstack installs the catalog into a generated project via npx skills add, not from an npm tarball; the diff publishes nothing from any released package. The precedent on this same file, #20778 (7a09eee1), landed with the same label, no .changeset entry, merged by the maintainer. No accept-set change, so the Clause-②: no line is well-formed (no arm, none owed).

③ Boundary flags

Dev report 5905964684 — every deviation answered:

  1. gh CLI absent; card/PR read by plain REST; writes only through scripts/pm (with-fleet pr_create, label-write, post-stamped), body read back byte-identical, no PATCH — answered, nothing to escalate: reads are unrestricted and the three writes took the sanctioned path.
  2. Three neighbouring sentences (:342, :344-345, :352) edited beyond the two card rows — answered: inside the dispatch's file surface ("any other sentence … that describes the api trigger or its secret"); each judged true in ① (items 4, 5, 7) and each deletion has a home; without them the ceiling would have been breached.
  3. No request recipe added — answered in ①.

open_questions: none declared, none found.

out_of_scope_findings (both content/docs/automation/**, carrier: none) — answered and escalated to the dispatching seat, not blocking: the dispatch reserved content/docs/** for census-only reporting, so noting them in the PR's acceptance notes is the right shape under Prime Directive #10. Classification holds: webhooks.mdx:733-736 is stale, not false; flows.mdx:92 is true but names neither the hook nor the required secret — it is the docs-lane twin of this card's :51 and a candidate for one docs-lane card (the webhooks.mdx note could ride it). Left to the seat to card.

Other flags: Tier H (skills/**) — this record is the seat's review of record; landing still takes an authorized APPROVED review and the maintainer's hand, and no seat readies, queues or arms it. Non-blocking note for the maintainer: :51's "every api flow" carries the resolver-precedence caveat in ① item 1; a qualifying clause would cost tokens the ceiling (headroom 3) does not have.

Check-runs on the head, read 2026-09-30T07:11:41Z — 34 runs: 20 success, 11 skipped, 2 in_progress, 1 failure.

  • in_progress (not a pass, recorded as read): Lint & Repo Gates (required; run 36681172476) and Test Core (1/6) (a shard of the required Test Core; run 36681172464). Shards 2/6–6/6 are success.
  • failure: Check Changeset on the opened run (36681172216, 06:59:31Z) — its event payload predated the skip-changeset label; the labeled run of the same job (36681380057) is skipped. pr-automation.yml:257-263 names this stale-red-by-construction pair itself. Advisory: not one of the seven required contexts.
  • Required contexts success: TypeScript Type Check, Dogfood Regression Gate, Governed Surface Queue Guard. Build Core and Temporal Conformance (live PG + MySQL) are skipped by paths filter on a docs-only diff. Type Check · source gates, the job carrying check:skill-docs / check:skill-refs (lint.yml:5502), is success.

Implemented-by: claude/issue-20569-automation-skill-api-secret
Reviewed-by: session_01KTZmMfzVzjNvyaLyQ8mHvg

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)— PR #20796 · automation 技能:api 流程的 secret 必填(#20569)

skills 席 1 · session_01KTZmMfzVzjNvyaLyQ8mHvg · 2026-09-30T07:16Z · 所审 head 32847a29

改了什么:只改 skills/objectstack-automation/SKILL.md 一个文件(+5 / −6,净 −1 行)。

  • Flow Types 表的 api 行:改为「入站 webhook」。每个 api 流程都绑定到自己的 hook 端点,必须有 start 节点的 secret;只靠显式调用的流程应写成 autolaunched。
  • 入站 webhook 一节的 secret 行:从「强烈建议」改为必填。没有非空 secret 的流程在注册时被拒,os validate 也会报错,启动时不会挂上;签名放在 x-objectstack-signature 头里。
  • 同节顺手改了三句相邻的话,把 token 付回来,每处删掉的内容在同一文件里都另有归属。

为什么改:PR #20551 之后,运行时在每个入口都拒绝没有 secret 的 api 流程,技能却还写着「可选」「可只显式调用」。AI 照着写,会写出一个每个入口都拒收的流程。

风险与代价(含回滚):纯文档,不动代码,不发布任何包(skip-changeset 已挂)。没有新增任何请求构造细节:路由、头名、签名格式在 main 上本来就写着。token 余量剩 3。回滚:revert 本 PR。

席位意见:ACCEPT,建议批准。

  • 契约复核 PASS(评论 5906148151,由隔离的达档子代理出具,席位核验后采纳)。复核在代码层逐个入口核实了每一句:引擎注册、启动、trigger-api、/automation 写入、os validate、/meta。
  • CI:Lint & Repo Gates 仍在跑,落地前须转绿。唯一的红是打标签之前那次 Check Changeset 的旧结果,打标签后那次已跳过。
  • content/docs 里 flows.mdx:92 对 api 的描述不完整(没提 hook 和 secret),但没有说错,本 PR 不改,留给 docs 车道下次编辑。

你要做的:在本 PR 上给 APPROVED。之后由本席位核对 CI 全绿,再翻 ready 入队。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 30, 2026 08:26
@os-zhuang
os-zhuang enabled auto-merge September 30, 2026 08:26
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit eac538c Sep 30, 2026
43 of 44 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-20569-automation-skill-api-secret branch September 30, 2026 08:52
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/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants