Skip to content

feat(spec): an api flow's start node may name its hook secret (config.secretRef), resolved per post - #25703

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-25675-api-flow-secret-ref
Oct 24, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-25675-api-flow-secret-ref

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #25675

Clause-②: yes (widening)
Metadata-protocol: added
Ruling-ref: 6386630545

An api flow's start node may now name its inbound hook secret instead of carrying it: config.secretRef: 'NAME'. The deployment fills that name, os validate and the engine accept the reference, and the engine resolves it at every inbound post. An app package can ship an inbound api flow without publishing a credential, and the repository's one literal hook secret (the showcase webhook flow) is gone.

Ruling 6386630545 (batch #12 item 3, letter A), verbatim:

「api 型流程的起始节点可以声明密钥引用(流程凭据名 / 环境变量名)而不是字面值;部署方填值,os validate 接受引用,引擎每次入站请求按引用解析,复用 #20790 已有的写入式流程凭据存储与 resolveSecret。公开仓库里不再出现任何凭据。」

What changes

@objectstack/spec: the key. packages/spec/src/automation/api-trigger-secret-ref.zod.ts (new) declares ApiTriggerSecretRefSchema: a name of the shape [A-Za-z_][A-Za-z0-9_]*, never in the platform's OS_ namespace. Both rules are patterns, so the published JSON Schema carries them too. FlowSchema's superRefine runs apiTriggerSecretRefRefusals on every start node and refuses, at nodes[i].config.secretRef:

  • a reference that is not a name;
  • one beside a literal secret. Two sources for one credential, and the literal would publish it;
  • one on a flow that binds no inbound api trigger (resolveFlowTriggerKind). Nothing would ever read it.

apiTriggerSecretRefOf(config) is the one reader of the name. The lint rule and the engine both call it, so neither restates the grammar.

@objectstack/lint: flow-api-trigger-secret-missing accepts a usable reference. It accepts one at all three commands and at the runtime publish gate, with no restored-credential position needed. A malformed reference is still "no secret". The rule's one-line remedy and its os explain entry now name config.secretRef.

@objectstack/service-automation: resolution per post.

  • hookSecretResolver gives a start node with a usable secretRef a reader. At every call the reader asks the credential resolver for that name. It is never cached and never asks the channel. A name that resolves to nothing, blank or whitespace rejects with FlowCredentialUnresolvableError (SERVICE_UNAVAILABLE / 503). The error names the flow and the reference, never a value. trigger-api already answers that rejection 503 and logs it, so an unfilled reference refuses every post and never admits one.
  • The resolver is the plugin's existing credentialResolver, the one a declarative connector's auth.credentialRef goes through (ADR-0097 §3). It moved to credential-resolver.ts (re-exported from plugin.ts unchanged) and is handed to the engine through the new AutomationEngineOptions.credentialResolver. The default is the open tier's environment variable.
  • The registration check passes on that reader. Its refusal text now names config.secretRef first.
  • The write-only flow credential channel gives the start-node position a fifth form, referenced. Saving a flow that names its secret removes any hook-secret row the channel held for that node, the way a cleared position does, so no stale credential stays at rest. heldPaths does not report it.

@objectstack/trigger-api: the arm-time refusal and the docblock say a secretRef is resolved by the automation engine. The trigger never reads the name itself, so a host that binds without that engine still refuses the flow.

Showcase: showcase_inbound_task_webhook now carries secretRef: 'SHOWCASE_WEBHOOK_SECRET', and the literal 'showcase-webhook-secret' and its "Demo secret" comment are removed. git grep showcase-webhook-secret (CHANGELOGs aside) finds 0 hits. The two platform-checklist items that signed with the old literal now export SHOWCASE_WEBHOOK_SECRET at boot and sign with it.

Docs: automation/flows.mdx (the credential callout said a packaged flow keeps its literal in source), automation/connectors.mdx (the resolver now serves secretRef too), and the regenerated reference page. The objectstack-automation skill's start-node table (it said a non-blank secret is required) is corrected in #25708, a governed skills/** PR that merges after this one. Only skills/objectstack-automation/references/_index.md rides here: it is gen:skill-refs output that check:skill-refs requires beside the spec change, and the generated-surface exception (#11705) lifts it.

Decisions taken (recorded for the reviewer)

  1. A name, resolved through the credential resolver, rather than a flow-credential-store name. The ruling allows "流程凭据名 / 环境变量名". Measured on origin/main: the flow credential store is keyed by position (flow, node, key), not by a name. Its only write door is the metadata save door, and a packaged flow cannot be overlaid (flow has allowEnvironmentOverlay: false). So a deployment has no door to fill a store entry for a packaged flow, and a store-name form would need a new write surface. The connector credentialRef seam already resolves a name per deployment: an environment variable in the open tier, or a host-wired secrets service. Reusing that seam serves both arms of the ruling with one dialect. The binding's resolveSecret (security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790) is the per-post reader, as ruled.
  2. A distinct key, not a tagged secret value. secretRef cannot be mistaken for the secret, and a literal beside it is refused.
  3. Not withheld on read. The reference is a name, not a credential. Withholding it would break the GET → edit → PUT round trip for no protection. Only config.secret stays in the redaction table.
  4. OS_ names are refused. Whoever signs the posts holds the hook secret, so a hook secret that is a platform secret (OS_AUTH_SECRET) would hand it out.
  5. An unfilled reference does not block registration. The deployment may fill it after deploy. Each post is refused 503 until then, with a located warn naming the flow and the reference.

Acceptance notes

Verification

All readings are at head 8a23b3ad, where origin/main b77bb37e is merged in. Test suites were last run at 1abed922 or earlier. The commits after that change only llms.txt counts (and the test-alias pin and docblock, which were re-run).

  • Package suites (full test, each through the verify lock): @objectstack/spec 0, @objectstack/lint 0, @objectstack/trigger-api 0, @objectstack/service-automation 0, @objectstack/metadata-protocol 0, @objectstack/runtime 0.
  • test:repo: @objectstack/spec 0 (79/79 files, 1269/1269 tests, after the docblock fix; the first run counted 32 demoted descriptions against the pinned 31), @objectstack/runtime 0, @objectstack/rest 0.
  • Typecheck: spec, lint, service-automation and trigger-api 0. example-showcase NOT MEASURED: ten TS2307 errors from workspace packages outside the build closure (service-print, plugin-approvals, connector-rest), none in a changed file. The showcase flow's new start node parses with FlowSchema against the rebuilt spec (true).
  • Spec generated artifacts: pnpm --filter @objectstack/spec check:generated, all 14 up to date after a build at the merged head.
  • Derived gates (dispatch-gates --commands, 128 families): 124 exit 0. Four exit 3, a refused prerequisite, NOT MEASURED: check-plugin-teardown-shape --self-test (shallow clone, needs --unshallow), spec check:skill-examples (client-react not built), check:dual-build-cjs-loads and check:i18n-coverage (the full workspace build). --ran reconciliation: 128 accounted, 124 run, 4 NOT-MEASURED, 0 UNRUN.
  • check-platform-checklist --changed-from origin/main: OK. check:nul-bytes: 0.
  • At head 088e4423 (the split, which only restores SKILL.md to origin/main): dispatch-gates --commands 125 derived, 121 exit 0, the same four NOT MEASURED, 0 UNRUN. Ablations on the two fail-closed branches (the unfilled-name 503 in service-automation, the beside-a-literal refusal in spec) each turned their pins red and were restored blob-identical (report addendum 6388889634 on An app package cannot ship an inbound api flow: config.secret must be a literal in source, and there is no deployment-time seam for a package-authored flow #25675).
  • Dogfood: n/a. The title has no ! and Clause-② has no (narrowing) arm.

Body edited by the domain:spec seat 2 after the governed split, as the dev proposed in 6388889634: the skill row and its readings moved to #25708, which carries its own 维护者速读; this PR's draft 维护者速读 is dropped, since its diff no longer needs a human merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8


Generated by Claude Code

claude added 10 commits October 24, 2026 16:51
…rence (config.secretRef)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
… config.secretRef

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
…cretRef per post through the credential resolver

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
…WEBHOOK_SECRET) instead of carrying a literal

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
…tion skill, and the changeset

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
…pens without a level-1 heading

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZJKktPWaz2EZJ95a9XZa8
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 24, 2026
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/lint, @objectstack/service-automation, @objectstack/spec, @objectstack/trigger-api, touching 26 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/services/service-automation/src/flow-credential-channel.ts, packages/spec/api-surface/automation.json, packages/spec/declaration-map/automation.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/connectors.mdx (via AutomationServicePluginOptions (symbol, a top-level interface))
  • content/docs/automation/flows.mdx (via AutomationServicePlugin (symbol, a top-level class), FlowSchema (symbol, a top-level const))
  • content/docs/deployment/self-hosting.mdx (via secretRef (literal, a string literal in API_TRIGGER_SECRET_REF_KEY; a string literal in a comment on a changed line))

⛔ 6 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class), FlowSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-4.mdx (via FlowSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-6.mdx (via AutomationEngine (symbol, a top-level class), AutomationServicePlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-7.mdx (via FlowSchema (symbol, a top-level const))
  • content/docs/releases/v18/18-1.mdx (via FlowSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/services/service-automation/src/flow-credential-channel.ts, packages/spec/api-surface/automation.json, packages/spec/declaration-map/automation.json, …) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 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 — 144 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 b1790841bb0bf2c7c696e858de51e8105704471a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 426240476fcf853e1d51c33c4b8fe370742dbb8f — the merge of head 088e442390cafbc51ac080fb3721626e440dfacf into base b1790841bb0bf2c7c696e858de51e8105704471a, 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 426240476fcf853e1d51c33c4b8fe370742dbb8f && git checkout 426240476fcf853e1d51c33c4b8fe370742dbb8f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b1790841bb0bf2c7c696e858de51e8105704471a 088e442390cafbc51ac080fb3721626e440dfacf && git checkout -B drift-repro b1790841bb0bf2c7c696e858de51e8105704471a && git merge --no-ff 088e442390cafbc51ac080fb3721626e440dfacf

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

⚠️ 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 b1790841bb0bf2c7c696e858de51e8105704471a → 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

席位核对

ACCEPT · #25675 · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T20:10Z · head 088e442390 · report 6388206801 and addendum 6388889634 (on #25675; cloud session session_01NZJKktPWaz2EZJ95a9XZa8) · seat review at TIER_DEFAULT. Clause-②: yes (widening) and the path limb hits (packages/spec/src/automation/**), so a same-shape ## Contract review PASS at CONTRACT_REVIEW_TIER on the landing head is owed. needs:contract-review is added in this stroke.

  1. Shape. Draft, base main, first line Fixes #25675. The Clause-②: yes (widening), Metadata-protocol: added and Ruling-ref: 6386630545 lines are line-initial in the body and in the changeset (spec, lint, service-automation, trigger-api, all minor). 35 files, +728/−52. No content/docs/releases/**.
  2. Governed reading, measured twice. check-governed-merges --pr 25703 from this seat's container exits 3: the skills/** hit is references/_index.md, and the [finding] skills/*/references/_index.md is generator-owned (gen:skill-refs) yet sits inside the skills/** governed-merge fence — every spec PR touching an indexed headline is forced to carry a governed-surface file #11705 recompute could not run on an uninstalled tree, so it fails closed. That is a refusal of the prerequisite, not a reading. The reading comes from CI's Governed Surface Queue Guard (job 120964600320), which installs dependencies: it LIFTED skills/objectstack-automation/references/_index.md as byte-equal to gen:skill-refs recomputed on that tree, so the net diff is not governed. That agrees with the dev's local run. SKILL.md is back to origin/main here. Its four branch commits that touched it show as "unattributed" on the PR-run early warning; the queue judges the landed commit, whose net diff no longer carries the file.
  3. The ruling, measured on the diff. Letter A of 6386630545: an api start node names its secret, the deployment fills it, os validate accepts the reference, and the engine resolves it on every inbound post.
    • config.secretRef (ApiTriggerSecretRefSchema, a name [A-Za-z_][A-Za-z0-9_]*, not OS_, both written as patterns so the JSON Schema carries them). FlowSchema refuses three shapes at nodes[i].config.secretRef: a reference that is not a name, one beside a literal secret, and one on a flow with no api trigger.
    • hookSecretResolver returns a reader that asks the credential resolver on every call, with no cache. A blank or missing value throws FlowCredentialUnresolvableError, which trigger-api already answers 503. An unusable reference, or one beside a literal, yields no reader, so registration refuses rather than arming.
    • "No credential left in the public repository": the showcase literal is gone (git grep showcase-webhook-secret finds 0 hits outside CHANGELOGs).
  4. Gates. ci-failure --pr 25703 GREEN: 42 check-runs on 088e442390, all completed, none failed. Local, per the addendum, on this head: 125 derived, 121 exit 0. Four exit 3 (NOT MEASURED, full-build or unshallow prerequisites), and --ran shows 0 UNRUN. Package suites (spec, lint, trigger-api, service-automation, metadata-protocol, runtime, plus test:repo for spec, runtime and rest) are green at 1abed922 or earlier; the two later commits are an llms.txt count and the split. Ablations, each restored blob-identical:
    • Disabling the unfilled-name refusal: api-trigger-secret-ref.test.ts goes from 5/5 to 2 red (the 503 pin and the env-default pin).
    • Disabling the beside-a-literal refusal: 8/8 to 1 red.
  5. Open questions.
  6. Deviations accepted. The dev did not edit the PR body after the split, citing the role file. The seat makes that edit in this stroke, as proposed in 6388889634. Also accepted: mcp_calls 3, all reads; relay writes 2 plus 2 this round. The two out-of-scope notes stay in the PR's Acceptance notes / report with no carrier: a pre-security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790 stored literal carried forward beside a new secretRef is refused loudly, and an unfilled reference warns only per post.
  7. Landing. Not governed, 780 changed lines. hotlong and os-zhuang are still requested reviewers from when the PR touched SKILL.md; that request is not a merge gate for this file list. Once a CONTRACT_REVIEW_TIER PASS is on record and every check on the then-current head is green, the seat sends pr_ready + automerge_enable through the relay. docs(skills): the automation skill's inbound-hook table names config.secretRef #25708 follows after this merges, by the maintainer's hand.

Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 088e442390cafbc51ac080fb3721626e440dfacf
Local-runs: none
Scope: packages/spec/src/automation/api-trigger-secret-ref.zod.ts, packages/spec/src/automation/flow.zod.ts, packages/spec/src/automation/index.ts, packages/spec/src/automation/api-trigger-secret-ref.test.ts, packages/spec/src/type-alias-convention.pin.test.ts + packages/lint/src/validate-flow-trigger-readiness.ts, packages/lint/src/rule-explanations.ts, packages/services/service-automation/src/flow-definitions.ts, packages/services/service-automation/src/engine.ts, packages/services/service-automation/src/credential-resolver.ts, packages/services/service-automation/src/plugin.ts, packages/services/service-automation/src/flow-credential-channel.ts, packages/services/service-automation/src/flow-credential-projection.ts, packages/services/service-automation/src/flow-registration-checks.ts, packages/services/service-automation/src/api-trigger-secret-ref.test.ts, packages/triggers/trigger-api/src/api-trigger.ts, examples/app-showcase/src/automation/flows/index.ts, docs/qa/platform-checklist/areas/automation.json, packages/spec/api-surface/automation.json, packages/spec/declaration-map/automation.json, packages/spec/export-origins/automation.json, packages/spec/json-schema.manifest/automation.json, packages/spec/llms.txt (prose faces .changeset/25675-api-flow-secret-ref.md, content/docs/**, generated skills/objectstack-automation/references/_index.md read, not scope)

① Derived judgments

  • Declaration is right. Clause-②: yes (widening) + Metadata-protocol: added: one new optional authorable key, config.secretRef, on an api flow's start node. Every refusal the PR adds is anchored on that new key (not a name, beside a literal secret, on a flow with no api trigger); nothing a published version accepted without the key is refused now, so no (narrowing) arm is owed and no dogfood battery is owed. Ruling-ref: 6386630545 is batch Add comprehensive test suite for Zod schema validation #12 item 3 letter A, quoted verbatim in the PR, the card and the changeset.
  • Letter A, measured against the diff. The ruling: 「起始节点可以声明密钥引用(流程凭据名 / 环境变量名)而不是字面值;部署方填值,os validate 接受引用,引擎每次入站请求按引用解析,复用 security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790 已有的写入式流程凭据存储与 resolveSecret。公开仓库里不再出现任何凭据。」 Each clause has a line in the diff: the key (ApiTriggerSecretRefSchema); os validate accepts it (describeUnusableSecret asks the spec's own reader apiTriggerSecretRefOf, so lint restates no grammar); the engine resolves per post with no cache (hookSecretResolver returns a reader that calls credentialResolver(ref) on every call); resolveSecret on the trigger binding is the reused security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790 per-post reader; the showcase literal is gone (0 hits outside CHANGELOGs).
  • The open question the seat handed this review: which name the reference resolves as. The ruling's parenthetical offers two arms, 「流程凭据名 / 环境变量名」. The PR takes the environment-variable arm through the automation plugin's existing credentialResolver (the connector auth.credentialRef seam, ADR-0097 §3; env var in the open tier, a vault where the host wires one). The measured facts justify it: the security(flows): move a flow's inbound-hook secret out of flow metadata into the write-only secret seam #7799 established — no read, the generic data door included, returns it #20790 store is keyed by position, its only write door is the /meta save, and a packaged flow cannot be overlaid (allowEnvironmentOverlay: false), so a store-name arm has no door a deployment can reach today and would need a new write surface. The store is still reused where it matters (a referenced position removes any stale held hook-secret row; heldPaths omits it). Reading holds: the ruling names the env-var arm explicitly, and the lighter arm that covers the case is the one the maintainer prefers first. A store-name arm, if ever wanted, is a superset follow-up with its own deployment-side write door, not a correction of this PR.
  • Fail-closed in both directions. An unfilled name rejects with FlowCredentialUnresolvableError, which trigger-api already answers 503 and logs naming flow and reference, never a value; a reference the spec refuses, or one beside a literal, yields no reader, so the flow is refused at registration rather than armed on a guess. Both legs are ablation-pinned (5/5 → 2 red; 8/8 → 1 red, restored blob-identical, addendum 6388889634).
  • The reference is a name, not a credential, so it is served on read and only config.secret stays in the redaction table. Right: withholding it would break GET → edit → PUT and protect nothing.
  • JSON Schema parity. Both rules are regex patterns, not refinements, so the published JSON Schema carries them (json-schema.manifest/automation.json updated; no new dropped-refinements site).
  • Governed reading. SKILL.md split out to docs(skills): the automation skill's inbound-hook table names config.secretRef #25708 (Tier H, maintainer's hand, merges after this one). The remaining skills/** path is gen:skill-refs output that check:skill-refs forces beside the spec change; CI's Governed Surface Queue Guard (job 120964600320) lifted it under the [finding] skills/*/references/_index.md is generator-owned (gen:skill-refs) yet sits inside the skills/** governed-merge fence — every spec PR touching an indexed headline is forced to carry a governed-surface file #11705 generated-surface exception on an installed tree, so this PR's net diff is not governed. Consistent with the seat's reading; the two still-requested reviewers are a leftover of the pre-split head, not a merge gate for this file list.

② Semver level

minor on @objectstack/spec, @objectstack/lint, @objectstack/service-automation, @objectstack/trigger-api is correct for Metadata-protocol: added with new exports and a new engine option; no !, no BREAKING section owed, a literal config.secret keeps working. All 54 check runs on head 088e442390 are success or skipped (Test Core 1–6/6, Build Core, Build Docs, Dogfood Regression Gate, Type Check ×5, Lint ×9, Check Changeset ×3, Governed Surface Queue Guard, Spec property liveness, claim gates); none red, none pending.

③ Boundary flags

Implemented-by: session_01NZJKktPWaz2EZJ95a9XZa8
Reviewed-by: session_01Bf4vhia9fHNXpqC1PCfxie

VERDICT: PASS

needs:contract-review removed in this stroke. Seat (domain:spec 2): pr_ready + automerge_enable when ready; #25708 after.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 24, 2026 21:15
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 24, 2026 21:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit fa44dd2 Oct 24, 2026
58 checks passed
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

3 participants