Skip to content

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

Closed

Description

@objectstack-fleet

Ruled: 6386630545 · batch #12 item 3 · letter A · 2026-10-24T16:24:48Z (an api flow's start node may declare a secret reference the deployment fills; os validate accepts it, the engine resolves per post via the #20790 store / resolveSecret; spec S p1 + server M)

Filing gate ② — a maintainer decision (a new authoring surface for package-shipped inbound hooks).

Filed for the hotsrm app (objectstack-ai/hotsrm, a public metadata-only SRM app on 18.0.0) by session session_01EGq2qAS1FBx325AAkDLPDH, while building its SCAR overdue sweep (hotsrm issue #37).

维护者速读

A public app package that wants an external scheduler to call one of its flows has no way to ship that flow. type: 'api' needs a non-blank literal config.secret on the start node or os validate refuses the package (flow-api-trigger-secret-missing) and the engine refuses the flow at registration. A literal secret in a public repository is a published credential. The write-only flow credential store (#20790) is reached only through the /meta save door, so it does not help a package built with os build. The decision asked for: how a package-authored api flow declares "the secret is supplied by the deployment".

What happens (measured)

  • hotsrm declares srm_scar_overdue_heartbeat (type: 'api', runAs: 'system', start node without config.secret) that calls the hourly schedule flow as a sub-flow, so an external scheduler can run the sweep every 10 minutes.
  • os validate (18.0.0) refuses it: flow "srm_scar_overdue_heartbeat" › start node: binds the inbound api trigger (type: 'api') but its start node declares no config.secret, so the engine refuses to register the flow (flow-api-trigger-secret-missing).
  • The alternatives are closed too: POST /api/v1/automation/:name/trigger on the runAs: 'system' schedule or autolaunched flow answers 403 (refusesElevatedSelfTriggeredStart, SELF_TRIGGERED_FLOW_TYPES = autolaunched / record_change / schedule).
  • The showcase flow says "Demo secret — real deployments inject this from configuration" (examples/app-showcase/src/automation/flows/index.ts), but no injection seam exists for a package (also noted in trigger-api arms a flow's inbound hook without a secret and accepts unsigned posts; ADR-0041's trigger-api acceptance criteria name a per-flow secret and HMAC verification #20529, closed).

Business reason

Options (for the maintainer, not a ruling)

  • A. The start node may declare a secret reference instead of a value (for example a sys_secret / flow-credential name, or an env var name), which the deployment fills; os validate accepts the reference and the engine resolves it per post, like resolveSecret already does for flows saved through /meta.
  • B. Allow an authenticated, permission-gated start of an elevated flow through /automation/:name/trigger when the flow opts in (a declared capability the caller must hold), so a service account can drive it without HMAC.
  • C. Keep today's rule and document that package-shipped inbound hooks are unsupported; packages publish the flow without the api trigger and each deployment adds its own signed wrapper in Studio.

四棱

Reader who acts: the triage seat grades it and puts it to the maintainer as a decision (needs-user-decision).

Dedupe: search_issues on objectstack-ai/objectstack, open and closed — api trigger secret env (1 hit: #20529, closed, unsigned posts), "config.secret" flow inject (5 hits: #24530, #20790, #20552, #20529, #20611 — all about storing/redacting a secret for flows saved through /meta, none about package-shipped flows), api flow secret package deployment credential store literal (8 hits, none on this). No carrier.

hotsrm keeps the gap as a documented, temporary omission (hourly schedule only) until this is decided.

Activity

  1. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first touch — enhancement · security · priority:p2 · domain:spec · area:workflow · needs-user-decision · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-24T16:00Z

    ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. os-elon-musk commented on Oct 24, 2026

    @os-elon-musk
    Collaborator

    Ruling: batch #12 item 3 · letter A · maintainer 「25635 25647 25675 同意」 2026-10-24T16:24:48Z

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

    工作量:domain:spec S(一个引用键,协议新增 → p1 按分诊口径)+ domain:server M(校验接受引用、按次解析、引用缺值时的拒绝)。前提修正已记在简报:托管云 Team 及以上套餐今天已按套餐放行定时流程(#25442 改裁 D),所以 hotsrm 心跳的紧迫性低于卡面;hotsrm 在此之前维持小时级定时。B(能力门控的 trigger 启动)与 C(维持不支持)未选。

    维护者动作已完成:needs-user-decision → pm:queue;裁不派。批 #12 简报:https://claude.ai/artifact/YLrSzMXcnWVmbkZCxgxcQt


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2
    Session: session_01PC4NPXPgPmpDRG9DdDnyXT
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25675-api-flow-secret-ref
    Worktree: objectstack-issue-25675
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: one cross-domain PR. The server charter's red line ("触 packages/spec 的卡一律归 spec 席") puts it in this lane; the cross-lane note goes to seat post #6021.
    (1) packages/spec: on an api flow's start node, a secret reference the deployment fills (a flow-credential name or an environment-variable name) in place of a literal config.secret. The dev locates the declaring schema and names it. Also its tests and regenerated shards.
    (2) packages/lint: flow-api-trigger-secret-missing (authoring-rules.ts about :1409–:1472, validate-flow-trigger-readiness.ts) accepts the reference.
    (3) domain:server: the engine resolves the reference per inbound post through the existing write-only flow credential store (#20790) and resolveSecret (packages/services/service-automation/src/flow-credential-channel.ts, engine.ts, the api-trigger secret channel), and refuses a post whose reference has no value.
    (4) The showcase "Demo secret" comment (examples/app-showcase/src/automation/flows/index.ts) if the reference replaces it, plus tests and one changeset .changeset/25675-api-flow-secret-ref.md.
    (stop on breach; explain in the report)
    Container & model: L, mode:cloud (spec plus lint plus service-automation: build-heavy, one card per container), model: default (TIER_DEFAULT; dispatch-gates --tier for this surface: no path-derived mandate), effort: xhigh (security; a credential boundary). Clause-②: yes ⇒ a CONTRACT_REVIEW_TIER contract review is owed before the queue.
    Clause-②: yes (widening)
    Metadata-protocol: added
    Responsibility: n/a — not a defect card
    Ruling-ref: 6386630545 (batch #12 item 3, letter A, maintainer 「25635 25647 25675 同意」)
    Thread-read: 6386630545
    Serial constraints cleared: none. No open PR touches these files (all 15 open PRs' file lists read 2026-10-24T16:43Z), and no open pm:dispatched claim declares them. PR #25643 (#25580, f1ddace887) changed the flow start doors in runtime, and it has landed.
    Premise: holds on origin/main 6021b5e837. flow-api-trigger-secret-missing refuses an api start node with no non-blank literal config.secret (validate-flow-trigger-readiness.ts, authoring-rules.ts about :1409). The flow credential store is reached only through the /meta save door (flow-credential-channel.ts), so a package built with os build cannot ship an api flow without publishing a literal.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatched · round 2 · domain:spec seat 2 · 2026-10-24T16:48Z

    • Back end: mode:cloud, cloud session session_01NZJKktPWaz2EZJ95a9XZa8 (created by this seat's session through create_session), on branch claude/issue-25675-api-flow-secret-ref. One cross-domain PR; the server surfaces are declared on seat post [PM seat] domain:services — ⏳ vacant · archive of domain:server seat 1 (#6367) #6021. The claim 6386845783 stays the identity.
    • Order given: the ruling verbatim (6386630545, letter A) with its premise correction, the lint and engine leads, the redaction question, the security floor (an unfilled reference refuses, no literal credential in the repository), the consumer-package test list, the Writes: line.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 25675,
     "status": "done",
     "branch": "claude/issue-25675-api-flow-secret-ref",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/25703",
     "session": "session_01NZJKktPWaz2EZJ95a9XZa8 (dispatched by session_01PC4NPXPgPmpDRG9DdDnyXT)",
     "premise_still_valid": true,
     "summary": "Premise re-measured on origin/main 6021b5e: it holds. flow-api-trigger-secret-missing and the engine's validateApiTriggerSecret both require a literal config.secret, and the flow credential channel is keyed by position with the /meta save door as its only write door, while flow has allowEnvironmentOverlay:false, so a packaged flow cannot reach it. Implemented ruling A. (1) spec adds start-node config.secretRef (ApiTriggerSecretRefSchema: [A-Za-z_][A-Za-z0-9_]* and not OS_, both as patterns). FlowSchema refuses a malformed reference, one beside a literal secret, and one on a flow that binds no api trigger. apiTriggerSecretRefOf is the shared reader. (2) lint accepts a usable reference, and the remedy and os explain name it. (3) service-automation resolves the name at every post through the plugin's existing credentialResolver (the connector credentialRef seam, ADR-0097 §3: env var in the open tier), passed to the engine as the new AutomationEngineOptions.credentialResolver. An unfilled name rejects with FlowCredentialUnresolvableError, which trigger-api answers 503 and logs with the flow and the reference. The channel gains a 'referenced' position form, so saving a referencing flow removes a stale held hook secret. (4) trigger-api messages. (5) The showcase literal 'showcase-webhook-secret' is replaced by secretRef 'SHOWCASE_WEBHOOK_SECRET' (0 git grep hits left outside CHANGELOGs), and the QA checklist, docs, the automation skill row and the changeset are updated. Not subscribed to PR activity: the role file's contract overrides the platform's subscription posture.",
     "tests": "HEAD 8a23b3ad (merged origin/main b77bb37e). Package suites through os-verify-lock: spec test exit 0; spec test:repo exit 0 (79/79 files, 1269/1269 tests) after a fix: the first run was red, file-description.test.ts expected 31 demoted descriptions and got 32 because the new module's docblock opened with a level-1 heading, so the docblock was changed and the pin left alone; lint 0; trigger-api 0; service-automation 0; metadata-protocol 0; runtime test 0; runtime test:repo 0; rest test:repo 0. Typecheck: spec, lint, service-automation and trigger-api 0; example-showcase NOT MEASURED (10 TS2307 from unbuilt workspace packages service-print, plugin-approvals and connector-rest, none in a changed file), and the showcase start node parses with FlowSchema against the rebuilt spec (true). New pins: spec api-trigger-secret-ref.test.ts (8), service-automation api-trigger-secret-ref.test.ts (5: per-post resolution and rotation, unfilled giving SERVICE_UNAVAILABLE/503 for undefined, '' and whitespace, the default env resolver, the channel never asked, refused references never armed), a flow-credential-channel test (a referenced position drops its held row and heldPaths omits it), lint (2 cases) and trigger-api (1 case). No ablation run: the behaviour is new, so no existing tested behaviour changed.",
     "gates": "dispatch-gates --commands at 8a23b3ad: 128 families, each recorded as 'cmd :: exit N' with $? captured before any pipe. 124 exit 0. 4 exit 3 = NOT MEASURED (refused prerequisite): check-plugin-teardown-shape --self-test (shallow clone), spec check:skill-examples (client-react dist absent), check:dual-build-cjs-loads and check:i18n-coverage (full workspace build). --ran reconciliation: '128 derived, 124 run, 4 NOT-MEASURED, 0 UNRUN', exit 0. Fixed on the way: check-skills-token-ratchet (SKILL.md +8 over its ceiling, fixed by genuine deletion: 5755 -> 5771 tokens, ceiling 5780, 438 -> 438 lines; pack 4389 -> 4389 lines, ratcheted tokens 144196 -> 144212), check:spec-parsed-alias (ADR-0122: z.input alias plus an isomorphic pin, 789 -> 790), check:quick-reference-counts (Automation 13 -> 14), spec check:llms-txt (205 -> 206, automation 13 -> 14), and the spec dropped-refinement gate (refine replaced by a projecting pattern). spec check:generated: all 14 artifacts up to date after a build at the merged head. check-platform-checklist --changed-from origin/main: OK. check:nul-bytes: 0. CI check-runs on #25703: in_progress at report time, not awaited.",
     "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
     "mcp_calls": "3 — mcp__github__issue_read x2 (get, get_comments on #25675) and ToolSearch to load it; no MCP write",
     "api_writes": "2 relay writes: fleet-write pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft, with assignee sunhaolin; request fw-20261024T185308Z-bff548, run 40122756265, read-back identical, 9901 bytes) and this os-dev-report comment (post-stamped, POST /repos/objectstack-ai/objectstack/issues/25675/comments). git push is not counted.",
     "open_questions": [
      {
       "question": "Which name the reference resolves as. The ruling allows a flow-credential name or an environment-variable name.",
       "options": [
        "A: a name resolved through the automation plugin's CredentialResolver (env var in the open tier, a host-wired secrets service otherwise), the same seam as connector auth.credentialRef (implemented)",
        "B: a name in the #20790 flow credential store, which is keyed by position and whose only write door is the /meta save door, closed to packaged flows (allowEnvironmentOverlay:false), so it needs a new write surface"
       ],
       "recommendation": "A. Real business need: hotsrm's deployment fills secrets by environment, and B has no door a deployment can reach today. Long-term soundness: one reference dialect (credentialRef/secretRef) through one resolver, which enterprise can back with a vault. Guarding against AI mistakes: one closed name grammar, refused loudly at FlowSchema. Startup scope: no new store surface. The binding's per-post resolveSecret from #20790 is reused as ruled; the store itself is reused only in that a referencing save removes its stale row."
      },
      {
       "question": "Is config.secretRef withheld on read like config.secret?",
       "options": [
        "A: not withheld; it is a name, not a credential (implemented)",
        "B: withheld"
       ],
       "recommendation": "A. Withholding it breaks the GET -> edit -> PUT round trip and protects nothing, and only the literal stays in FLOW_NODE_CREDENTIAL_KEYS."
      },
      {
       "question": "Refusing names in the OS_ namespace is an extra constraint the ruling does not state.",
       "options": [
        "A: refuse OS_* (implemented): whoever signs posts holds the hook secret, so OS_AUTH_SECRET reused as a hook secret would be handed out",
        "B: accept any identifier, as connector credentialRef does"
       ],
       "recommendation": "A, a contract tightening on a new key with no producer yet. Say if it should be dropped."
      },
      {
       "question": "Governed surface: this PR edits skills/objectstack-automation/SKILL.md (Tier H), which the claim's file surface did not list.",
       "options": [
        "A: keep the one-row fix in this PR (done): the row said a non-blank secret is required, which this change made false, and the role file says a published sentence the change makes false is fixed in the same PR",
        "B: split it into a docs PR"
       ],
       "recommendation": "A. The PR is now Tier H and needs the maintainer's approval as well as the Clause-② contract review. The PR body carries the 维护者速读 section and the skill readings."
      }
     ],
     "out_of_scope_findings": [
      "class: none (note) · reach: n/a · A stored flow row from before the #20790 migration that still holds a literal secret: if an author edits it to secretRef through /meta, the carry-forward would restore the literal beside the reference and FlowSchema refuses the save. Loud, not silent; the boot migration moves such literals out first. carrier: none — in the PR's Acceptance notes only.",
      "class: none (note) · reach: n/a · A reference left unfilled is reported only per post (a warn on each refused post), with no boot-time line saying the hook cannot verify. carrier: none — noted, not filed."
     ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report — round 2 addendum (patch round 1 for #25703)


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim addendum (amends the file surface of 6386845783, round 2) · domain:spec seat 2 · 2026-10-24T20:09Z

    The claim's surfaces (1) to (4) held. PR #25703 also touches the following, each forced by the change:

    Split out, governed: the objectstack-automation skill's start-node row (skills/objectstack-automation/SKILL.md, Tier H) is PR #25708 on claude/issue-25675-skills-secret-ref-row, first line Refs #25675, with its own @objectstack/skills patch changeset. It merges after #25703; the maintainer merges it.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-24T21:54Z

    reading value
    PR #25703 MERGED through the queue at 2026-10-24T21:53:34Z; merge commit fa44dd2559. ACCEPT 6388986607; cross-account contract review PASS 6389260006 on head 088e442390 (the landing head). That review settled the open question this seat handed it: the env-var arm through credentialResolver is a reading of ruling 6386630545 that holds
    queue branch gh-readonly-queue/main/pr-25703-* gone (git ls-remote: 0 refs)
    origin/main carries fa44dd2559; packages/spec/src/automation/api-trigger-secret-ref.zod.ts holds API_TRIGGER_SECRET_REF_KEY (6 hits); the showcase flow carries secretRef: 'SHOWCASE_WEBHOOK_SECRET' (1 hit)

    State: the card closed completed on the merge (Fixes #25675). pm:dispatched is removed in this stroke.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report — round 3 addendum (PR #25708, merge-group token ratchet)

    • Merge: origin/main (4cc17db3) merged into claude/issue-25675-skills-secret-ref-row as merge commit fa387380, with no rebase and no force-push. main had rewritten the capability-prerequisite table in the same file.
    • Measured after the merge: skills/objectstack-automation/SKILL.md was 5793 tokens against a ceiling of 5780 (+13). That is the same reading as the merge-group failure (run 40141451659).
    • Paid by deletion, in the same file and with no re-wrap (head 61942712):
      1. The secretRef / secret row, from "HMAC-SHA256 shared secret, required — without one the flow is refused at registration (os validate too). Ship secretRef: 'NAME' (an env var the deployment fills, not OS_*; unfilled ⇒ 503), never a literal" to "HMAC-SHA256 secret, one required (else refused at registration and os validate). Ship secretRef: 'NAME': an env var the deployment fills, not OS_*; unfilled ⇒ 503. Never a literal". That saves 6 tokens, and every fact is kept: one of the two is required, an env var the deployment fills, not OS_*, unfilled ⇒ 503, never a literal.
      2. Deleted the parenthetical "(the start config is a free-form record with no Zod shape)" from the sentence above the table, which saves 16 tokens. It is no longer accurate, because config.secretRef is now validated by FlowSchema.
    • Predicted vs observed: predicted at least 13 down; observed 5793 → 5771 (−22), which leaves 9 tokens of headroom under the ceiling of 5780. The ceiling was not touched. Lines: 432 → 432.
    • Gates at 61942712:
      • check-skills-token-ratchet --self-test exit 0 (79 cases), and the ratchet itself exit 0.
      • pnpm --filter @objectstack/spec check:skill-refs exit 0, after a build under the verify lock.
      • dispatch-gates --commands derived 46: 44 exit 0, and 2 exit 3 NOT MEASURED (check:dual-build-cjs-loads, check:lean-entry-closure, both needing the full build). --ran: 46 accounted, 0 UNRUN.
    • The push probably dismissed os-zhuang's approval, so docs(skills): the automation skill's inbound-hook table names config.secretRef #25708 needs approving again. feat(spec): an api flow's start node may name its hook secret (config.secretRef), resolved per post #25703 was not touched.
    • Writes: git push plus this comment through the relay. MCP calls: 0.

    Generated by Claude Code

  10. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed, governed half · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-25T09:41Z

    reading value
    PR #25708 (skills/**, Tier H, Refs #25675) MERGED through the queue at 2026-10-25T09:40:16Z; merge commit d0eac355c3. os-zhuang (GOVERNED_APPROVERS) approved the landing head 6194271233 after the patch round for the skills token ratchet. ci-failure: GREEN, 44/44. check-governed-merges --pr 25708 read GOVERNED, landing tier H, "an authorized APPROVED review … and then the owning seat lands it". The seat then armed auto-merge through the relay (run 40180255856)
    queue branch gh-readonly-queue/main/pr-25708-* gone (REST matching-refs: 0 refs)
    origin/main carries d0eac355c3; the skills/objectstack-automation/SKILL.md row reads `secretRef` / `secret` "HMAC-SHA256 secret, one required … Ship secretRef: 'NAME'"

    State: the card closed completed with the code half (PR #25703, landing record 6390012662). With this, both halves are in main. pm:dispatched is already absent. The dev's cloud session session_01NZJKktPWaz2EZJ95a9XZa8 is archived in this stroke.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itdomain:specenhancementNew feature or requestpriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions