Skip to content

fix(service-automation): the http node's url and headers describes route an outbound credential to a connector's credentialRef, and the showcase says its webhook url is served (#20590) - #20672

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20590-credential-guidance
Sep 29, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-20590-credential-guidance

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20590
Clause-②: no

What this changes

This is the domain:services face of triage's direction A on this card (ruling 5891721503, "Direction: A, taken whole"), scoped by the claim 5891908897. It carries two guidance edits and nothing else. Nothing new is withheld on read.

  1. The http action descriptor's configSchema (packages/services/service-automation/src/builtin/http-nodes.ts). The url field read "Target URL" and the headers field read "Request headers". Both now say that the value is stored in the flow definition, that a flow definition is served to every member who can read flows, and that an outbound credential goes to a connector_action on a declarative connector whose auth.credentialRef names the secret. (Seat's correction after patch round 1: that holds for headers. For url, a query-string key goes to a declarative rest connector with api-key auth (paramName, auth.credentialRef), and a webhook whose path is the secret goes to a token-authenticated connector such as slack, since no credentialRef variant carries a path-borne secret.) Of this node's config, only signingSecret is withheld on read (FLOW_NODE_CREDENTIAL_KEYS), so url and headers are served as authored. A code comment beside the two fields records that.
  2. The showcase's webhook-url teaching site (examples/app-showcase/src/automation/flows/index.ts, the slack_post node in showcase_fan_out_notify). The branch comment used to teach "post through an incoming webhook". It now sends a real post through a connector, as TaskCompletedSlackFlow already does. A comment directly above the placeholder url says that the url is served with the flow definition to every member who can read flows, and that an incoming-webhook url's path is its secret, so a real one never goes there.

A changeset (patch, @objectstack/service-automation) covers the descriptor text, which ships in that package's dist. The showcase is private, so it takes no changeset.

Mechanism assumptions, measured at 992c9ac87

A1: where the descriptor text reaches an author. AutomationEngine.getActionDescriptors() backs the runtime automation domain's GET /automation/actions, which is the designer palette's feed. The client SDK calls it as automation.listActions. The configSchema it serves is what the Studio property form is built from (io-node-form-zod-ledger.test.ts header).

A2: the showcase site. The showcase could not simply take the connector route.

  • showcase_fan_out_notify is the platform checklist's parallel + http fixture (docs/qa/platform-checklist/areas/automation.json, automation.flow-node-type-matrix). That item locates the http variant's step in this flow's runs. Turning slack_post into a connector_action would move the tested surface.
  • A provider-bound connector with a real credentialRef fails the boot when the reference does not resolve (resolveInstanceAuth, ADR-0097 §3). That is why every showcase instance declares auth: { type: 'none' }. Adding one would also touch src/system/connectors/index.ts, which is outside this card's file surface.
  • So the url stays, and the comment beside it says plainly that it is served. The edit is comment-only: with whitespace minified, esbuild 0.28.1 compiles the file at the merge base and at 992c9ac87 to byte-identical JavaScript (sha256 prefix 6197a04e663ec0d8, 31,595 bytes). The positive control is a one-character change in the same url, which does change the output.

Tests, at 992c9ac87

  • @objectstack/service-automation: 154 test files and 1,913 tests passed (vitest run --maxWorkers=2). typecheck passed, including check:test-typecheck. tsconfig.test.json compiles http-nodes.test.ts (counted with --listFiles).
  • New pin in http-nodes.test.ts: the url and headers descriptions each name auth.credentialRef and connector_action. It asserts the named route, not the prose.
  • Ablation, with the fix committed first, through scripts/ablation-replace.mjs in wrap mode plus a shell trap. The mutation put both descriptions back to their pre-fix text. On disk: auth.credentialRef count 2 to 0, and the old one-line fields 0 to 1 each. Result: exactly the two new cases failed, "2 failed, 8 passed", with the messages "expected 'Target URL' to contain 'auth.credentialRef'" and "expected 'Request headers' ...". Restore proof: blob 67f3d9d05a64 equals the HEAD blob, and git diff HEAD is empty. The test imports the source file directly, so no build sits in the path.
  • Published surface: the built dist/index.js and dist/index.cjs each carry the new text twice, and the old headers description zero times. The positive control, the unchanged method description, appears once in each. files[] is dist, README.md, CHANGELOG.md.
  • @objectstack/example-showcase: typecheck passed, and the full suite passed: 29 files, 385 tests.
  • Lint, narrowed to the three touched .ts files: 3 files, 0 errors, 0 warnings (eslint --no-inline-config --format json). The narrowing is sound for three reasons. --print-config resolves a rule set for each file, so none is ignored. No parserOptions.project or projectService is set, so linting is not type-aware. A diff therefore cannot move any untouched file's verdict. The full pnpm lint is CI's.

Gates

node scripts/pm/dispatch-gates.mjs --commands at 992c9ac87 derived 63 commands. All 63 were run with each exit code captured before any pipe, and all exit 0. --ran reconciles "63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN", with every code recorded.

  • check:dual-build-cjs-loads and check:type-check-debt first answered PREREQUISITE NOT MET (exit 3). They were re-run after a turbo build of every package, and both then passed.
  • The four roster families were run separately, and each exited 0: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:error-code-casing and pnpm check:filter-alias-parity.
  • The derivation reports that origin/main has moved five commits past this branch's base. None of those commits touches a path in this diff.

Acceptance notes

Patch round 1 (the seat's append)


Generated by Claude Code

…ute an outbound credential to a connector's credentialRef, and the showcase says its webhook url is served (#20590)

The http action descriptor's configSchema is the designer palette's
authoring form. Its url and headers fields now say that the flow
definition is served to every member who can read flows, and send an
outbound credential to a connector_action on a declarative connector
whose auth.credentialRef names the secret. The showcase's parallel+http
fixture keeps its placeholder webhook url, and the comment beside it now
says the url is served, so a real secret-bearing url never goes there.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 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 — 6 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 9a4b2bb38f9fb82c8e970d4f8d66d72379faa345 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 9a4b2bb38f9fb82c8e970d4f8d66d72379faa345

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…key to api-key credentialRef and a path-secret webhook to a token-authenticated connector (#20590)

No credentialRef variant carries a secret in the url path, so the url
describe no longer sends a path-secret webhook to auth.credentialRef.
A query-string key goes to a declarative rest connector with api-key
auth (paramName + auth.credentialRef); a webhook whose path is the
secret is replaced by a token-authenticated connector such as slack.
The code comment no longer claims the spec describes already carry the
steer, and the changeset names the three routes.

Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 29, 2026 17:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 29, 2026
Merged via the queue into main with commit 14f80e2 Sep 29, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20590-credential-guidance branch September 29, 2026 17:33
github-merge-queue Bot pushed a commit that referenced this pull request Sep 30, 2026
… a connector credentialRef and drops the Slack webhook-url example (#20778)

Fixes #20657

Clause-②: no

The skills half of the #20590 family (the parent is completed; the spec,
runtime and guide faces landed in #20654 / #20655 / #20672).
`skills/objectstack-automation` now tells an author what a flow
definition serves and where an outbound credential goes, and its long
example no longer teaches a secret-bearing Slack webhook url.

## What changed

**`skills/objectstack-automation/SKILL.md`**
- The `http` row of the node table now says: `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; routed by shape — a header credential → a
declarative connector (`bearer` / `basic` / `api-key`) with
`auth.credentialRef`; a query-string key → `api-key` with `paramName`; a
path-secret webhook url → a token-authenticated connector such as
`slack` — called via `connector_action`. The same row carries the
`timeoutMs` lesson that used to live only in the example's `http` node
(the executor arms a timer only when `timeoutMs` is set —
`packages/services/service-automation/src/builtin/http-nodes.ts:281`).
- The `script` note's dispatch sentence routes Slack to
`connector_action` on the `slack` connector instead of "or `http`
webhook".
- The CRM Blueprint's lead sentence carries the metadata-first rule —
"Default approach for metadata apps — model the business lifecycle in
Flow/Approval metadata first, reserve custom code for edge-case
integrations — in this CRM-style structure:" — because that rule had no
other home in the skill.
- Removed as token payment (content with a home elsewhere or redundant
in place, never a re-wrap): the `http_request` alias clause on the
`http` row — the clause was accurate: the ADR-0087 conversion
`flow-node-http-callout-rename`
(`packages/spec/src/conversions/registry.ts:58`) still rewrites
`http_request` / `http_call` / `webhook` to `http` on build/validate
even though the node-type enum
(`packages/spec/src/automation/flow.zod.ts:41`) names only `http`; the
skill teaches the canonical `http` and the alias needs no author action,
so the deletion stands; the `FlowNodeAction` seed-set aside; the
engine-internal `resolveTriggerBinding` name; the trigger-intro clause
that duplicated the Trigger Types table's ObjectQL-hook column; the "It
is not a way past a guardrail" one-liner that duplicated the paragraph
under the routing table; the three-line `previous` / `record` blockquote
reduced to a one-line pointer at objectstack-formula §5
(`skills/objectstack-formula/SKILL.md:283-296` carries it); the word
"verbatim" in the three moved-to pointers; and word trims in four
sentences.

**`skills/objectstack-automation/references/examples-flows.md`**
- The `notify_manager` node is a `connector_action` on the `slack`
connector (`chat.postMessage`, `input: { channel, text }`) — the shape
`examples/app-showcase/src/automation/flows/index.ts:329-339` dispatches
and `packages/connectors/connector-slack/src/slack-connector.ts:100-114`
declares. A three-line comment says why it is not an `http` node with a
webhook url. The block keeps its `os:check` marker and type-checks
(readings below).
- Removed as token payment: the four-line schedule comment is two lines
of the same facts, and the `filter` comment that duplicated the
blockquote at the top of the file.

Wording matches the landed sources: the `HttpConfigSchema.headers`
describe (`packages/spec/src/automation/io-node-config.zod.ts:363-370`,
the #20654 describe the dispatch could not locate; its sibling for
`connectorConfig.input` is `flow.zod.ts:628-634`), the `http` node
descriptor describes
(`packages/services/service-automation/src/builtin/http-nodes.ts:111-133`)
and the flows guide callout (`content/docs/automation/flows.mdx:274`,
whose "Only `signingSecret` (and a start node's `secret`) is withheld"
and "`bearer`, `basic` or `api-key`" the row now matches; the redaction
paths are `packages/metadata-protocol/src/metadata-redaction.ts:427` /
`:477` / `:521`). Per the wording guard on the card, no sentence routes
a path-secret webhook url to `auth.credentialRef`.

Not touched, by the card's serial constraint: SKILL.md's `api` trigger
row and the inbound-webhook `secret` row are #20569's; #20569 remains
open.

## `skills/**` readings

Tokens are `scripts/check-skills-token-ratchet.mjs` readings (ceil of
utf8 bytes / 4); lines are `wc -l`. Before = `c9c182ed`, after =
`6b3450a0` (the `skills/**` bytes are identical at `7d4fcc50`).

| File | Lines before → after | Tokens before → after (ceiling) |
|---|---|---|
| `skills/objectstack-automation/SKILL.md` | 448 → 439 | 5785 → 5777
(5785, headroom 8) |
| `skills/objectstack-automation/references/examples-flows.md` | 136 →
135 | 1436 → 1426 (1436, headroom 10) |
| Whole package — every `skills/**/SKILL.md` | 4404 → 4395 | ratcheted
authored total 143347 → 143329 |
| Whole package — every `SKILL.md` plus the touched reference file |
4540 → 4530 (net −10 against the PM's +4 budget) | bundle total 153900 →
153882 |

No ceiling row moved. `evals/flows-triggers-approvals.json` is untouched
(1255 / 1255).

## Verification (all on head `6b3450a0`; exit codes captured before any
pipe)

Head `6b3450a0` is `7d4fcc50` (the review-round commit) merged with
`origin/main` `91e8fa19`; the merge touched nothing under `skills/**`,
recorded no regen deferral, and the diff against `origin/main` is
exactly the two files above. The merge is the derivation tool's own
prescription: at `7d4fcc50` it reported a STALE TREE (`origin/main` had
moved to `eead9dcf` with `.github/workflows/release.yml` and
`scripts/release-pending-publish.mjs` changed, both inputs of derived
families).

- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` (no paths) derived the same 25 commands at `b6041881`,
`7d4fcc50` and `6b3450a0`; all 25 ran on `6b3450a0` with exit 0,
recorded as `cmd :: exit N`, and `dispatch-gates --ran` answers "25
derived families accounted for — 25 run, 0 NOT-MEASURED" with no
stale-tree warning: `check-ci-filter-parity`,
`check-closing-keyword-parity` (+ `--self-test`),
`check-comment-mask-corpus`, `check-doc-route-spelling --advisory` (+
`--self-test`), `check-skills-token-ratchet` (+ `--self-test`), lint
`check:doc-formula-expressions`, spec `check:skill-docs`, spec
`check:skill-refs`, root `check:agent-test-spelling`,
`check:corpus-claim-drift`, `check:cross-package-test-inputs`,
`check:doc-authoring`, `check:driver-memory-census`,
`check:gitlink-declared`, `check:nul-bytes`, `check:pm-governed-merges`,
`check:refd-timer-probe`, `check:role-word`,
`check:skill-compatibility`, `check:skill-frame-sync`,
`check:skill-identifier-liveness`, `check:watch-hint-literal`. The same
25 also ran on `7d4fcc50`, all exit 0.
- Implicated beyond the derived list: spec `check:skill-examples` (the
edited example carries an `os:check` marker): exit 0 on `6b3450a0` —
"259 prose examples type-check across 3 surface(s) — every marked block
parsed, so tsc ran the SEMANTIC pass on all of them" (skills + docs
surface: 225 blocks), after `turbo run build --filter=@objectstack/spec
--filter=@objectstack/formula --filter=@objectstack/lint
--filter=@objectstack/client --filter=@objectstack/client-react` under
the verify lock.
- Ablation of the edited block, both legs through
`scripts/ablation-replace.mjs`, run at `b6041881` on the
`examples-flows.md` blob `6e725a55` — the same blob at `6b3450a0`, so
the reading stands (anchor hit 1 → 0 on disk, blob hash changed,
restored to the `HEAD` blob with `git diff HEAD` empty; no build step
involved because the checker reads the markdown itself):
- `actionId:` → `actionIdX:` inside `connectorConfig`: exit 1 —
`examples-flows.md:56:9 error TS2561: Object literal may only specify
known properties, but 'actionIdX' does not exist in type '{ connectorId:
string; actionId: string; input?: ... }'` (the `input` type is a Record
of string to unknown, spelled in words here because the platform eats
angle-bracket fragments). The block's `connectorConfig` shape is
measured.
- `type: 'connector_action'` → `type: 'connector_actionX'`: exit 0,
still 259 green. The node `type` string is not constrained by the
type-check (plugin-registered node types such as `approval` mean it
admits any string), so that half of the example rests on the showcase
app and the spec enum, not on this gate. Reported as observed.
- A control-character grep (the C0 range and DEL) over both touched
files: no match. No HTML comment and no angle-bracket fragment is added
by this diff.
- `check:skill-docs` / `check:skill-refs` are green without
regeneration: the frontmatter is unchanged, so no generated artifact
moved.

## Changeset

`skip-changeset`, measured: `git grep -n -w skills -- '*/package.json'`
→ 0 hits at `6b3450a0` (positive control: `git grep -n '"files"' --
'packages/*/package.json'` hits `packages/adapters/hono`,
`packages/apps/account`, `packages/apps/setup`), so nothing under
`skills/**` ships in any released package's `files[]`. Precedent: the
last skills-only PR that touched these files (#19738) carried the same
label.

## Acceptance notes

- `SKILL.md` `:216` (a reference outside a filter — message body, `http`
url, write payload — renders an empty string) is a
template-interpolation statement, not a credential statement; left as
is, the `http` row carries the steer.
- The eval `evals/flows-triggers-approvals.json:47` expects an `http`
node to set `timeoutMs`; that lesson now lives on the `http` row (it
left the example when the node became a `connector_action`).
- `content/docs/automation/flows.mdx:274`, which the services seat's
wording guard said carried an over-reach, already routes by shape on
`main` at `c9c182ed`; nothing to correct, and this PR does not touch it.
- Noted, not filed: `check:doc-formula-expressions` and
`check:skill-examples` each refuse (exit 3) on a fresh worktree until
their producer packages are built; both say so loudly and name the
build. Working as designed.

## 维护者速读(草稿)

**改了什么**:`objectstack-automation` 技能的 `http` 节点行现在说明:flow 定义(含 `http`
节点的 `url` 与 `headers`)会原样提供给所有能读 flow 的成员——只有 `signingSecret` 与 start
节点的 `secret` 会被隐去——因此凭据不能写在那里;并按凭据所在位置给出去向:请求头凭据 → 声明式 connector 的
`auth.credentialRef`(`bearer` / `basic` / `api-key`),查询串密钥 → `api-key` 的
`paramName`,路径即密钥的 webhook → 带 token 的 connector(如 `slack`),再用
`connector_action` 调用。长示例里的 Slack incoming-webhook url 换成了 `slack`
connector 的 `chat.postMessage`,与 showcase
示例应用的写法一致。「元数据优先、自定义代码只留给边缘集成」这条规则并入了 CRM 蓝图的首句(它在技能里没有别的落脚点)。为守住 token
上限,删掉的都是别处已有归属或原地重复的内容(如 `previous` / `record` 的说明缩成指向
objectstack-formula §5 的一行)。

**为什么改**:这是 #20590 家族的技能面。AI 作者直接从这份技能复制,原示例教的是把秘密写进 url;spec
描述、运行时描述与文档已在别的 PR 落地同一措辞,技能面是最后一块。

**风险与代价(含回滚)**:纯文档改动,不动运行时、不发布任何包(`skip-changeset` 已实测)。示例块经
`check:skill-examples` 类型检查通过,并用消融证明 `connectorConfig` 的形状确实在检。SKILL.md
剩余 token 余量为 8,下一张改此文件的卡(#20569)仍需自付 token。回滚即 revert 本 PR,无迁移。

**席位意见**:(留空)

**你要做的**:`skills/**` 是 Tier H 受管面——需要维护者的 APPROVED review 后由席位落地;本 PR 保持
draft,不进队列、不开 auto-merge。

---
_Generated by [Claude
Code](https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg)_

---------

Co-authored-by: os-warren <warren@objectstack.ai>
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…osition draws an author-time advisory that routes it to a connector (objectstack-ai#20654) (objectstack-ai#20698)

Fixes objectstack-ai#20654
Clause-②: no

This PR is the `packages/spec` + `packages/lint` face of objectstack-ai#20590's
direction A (triage ruling `5891721503`, carried by the card and
restated in `5891910265`). The services face (the `http` descriptor text
and the showcase) is objectstack-ai#20590's own claim, and the docs and skill faces
are objectstack-ai#20655 and objectstack-ai#20657. objectstack-ai#20590 is not closed by this PR.

## What changes

A flow definition is served, as authored, to every member who can read
flows. The read path withholds the credential slots the spec declares,
but an open map or a url cannot be withheld: an ordinary value there is
indistinguishable from a credential. So this PR steers and warns.
Nothing is withheld and nothing is refused.

- **One predicate, one home.** `@objectstack/lint` gains
`isCredentialShapedLiteral(name, value)` in
`packages/lint/src/credential-literal.ts`. It is the measured scanner's
R1/R2 rule, unchanged: a credential-named key (R1) or an auth-scheme
value (R2), on a non-blank string that carries no `{…}` template. The
name lists are module-private. A test pins that no other shipped source
file in the package holds them.
- **One advisory.** `lintFlowCredentialLiterals` (rule id
`flow-credential-literal`,
`packages/lint/src/lint-flow-credential-literals.ts`) reports one
`warning` per credential-shaped literal in three positions: an `http`
node's `config.headers` entry, a query parameter of an `http` node's
`config.url`, and a node's `connectorConfig.input` at any depth. Nodes
nested in `try_catch` / `loop` / `parallel` regions are included. The
finding names the key and the position, and it never carries the value.
- **Routed by shape.** A header credential goes to a connector whose
`auth` is `bearer` or a header `api-key`. A query-string key goes to
`api-key` with `paramName`. A connector input goes to the connector's
own `auth.credentialRef`. The route is in the message, which is what the
`os validate` and `os lint` text faces print; the hint carries the full
declaration. No text promises a variant that carries a secret in a url
path. This follows the wording guard the services seat relayed in
`5894445934`.
- **Registered once.** There is one new `AUTHORING_RULES` entry, `tier:
'advisory'`, on all three commands, with `surfaces: ['cli',
'runtime-publish']` and `runtimeTypes: ['flow']`. The wiring guard's
"every advisory rule really is advisory" check reads the rule's source
for an `error` severity. The finding type is `severity: 'warning'`.
- **Describes.** `HttpConfigSchema.headers` and a flow node's
`connectorConfig.input` say the definition is served to every flow
reader, and route an outbound credential as above. The wording changes;
no shape changes.
`content/docs/references/automation/{flow,io-node-config}.mdx` are
regenerated with `gen:docs`.
- **Changeset.** `@objectstack/lint` minor (new exports and a new
advisory), `@objectstack/spec` patch (describe text only). Both
packages' `dist` carry the change: measured by grep of `files[]` after
the build, with a positive control.

## Mechanism hypotheses, measured

1. **Lit control on the base (`f4ce10c89d`): confirmed.** A flow with a
credential-shaped literal in an `http` header, a url query key and a
nested connector input got these answers:
- The save door (`evaluateRuntimeAuthoringGate`, `state: 'active'`)
returned `error: null, advisories: []`. The dark control (the same flow
with `{var}` templates) returned the same.
- The live-channel control, a `try_catch` with no `catch`, drew exactly
one advisory, `flow-try-catch-without-catch`.
- `os validate --json` exited 0 with `valid: true`. Its only warning was
`No apps or plugins defined — this stack may not do much`, lit and dark
alike.
- `os lint --json` exited 0 with `passed: true` and `issues: []`, lit
and dark alike.
2. **No door-side change: confirmed.** The registry entry alone reaches
all three doors. `packages/cli/src/commands/*` and
`runtime-authoring-gate.ts` are untouched.
3. **Published surface.** `packages/lint` has no api-surface or
export-origins shards. The `.` entry gains `isCredentialShapedLiteral`,
`lintFlowCredentialLiterals`, `FLOW_CREDENTIAL_LITERAL` and the type
`FlowCredentialLiteralFinding`. `./runtime` exports nothing new.
4. **Derived artifacts moved.**
- `check:docs-transcript-drift`: the four CLI transcripts print `Running
author-time rules (48)`, up from 47. There is no generator; each line is
set to the value the gate derives from `authoringRulesFor(cmd)`.
   - `check:docs`: the two reference pages above, via `gen:docs`.
   - No rule catalog exists.
   - `check:generated` reports all 15 artifacts up to date.
5. **Ablation: confirmed, in the predicted direction.** The ablation ran
at `f7fe981913` through `scripts/ablation-replace.mjs` (WRAP mode). It
made the predicate always return false, then rebuilt
`@objectstack/lint`.
   - The dist preflight found the marker in 4 built files.
- The lint pins went red on 33 tests: all 27 positive predicate cases
and 6 rule pins (the pin set, severity, wording, array walk, reach
through `runAuthoringRules` for each command, reach through
`runRuntimeAuthoringRules`). The negatives, the draws-nothing controls,
the registry entry, the export and the one-home pin stayed green, 21 in
all.
- The save-door pin went red on LIT and stayed green on DARK and draft.
The `os validate` and `os lint` pins both went red.
- Restore: the blob matched HEAD, `git diff HEAD` was empty, the marker
was absent from all 14 dist files and the tree was clean. Then lint
54/54, save door 3/3 and CLI doors 2/2 all passed.

## Verification record (HEAD `ae5d8af6f8`, which includes current
`main`)

- The door readings after the change, on the same lit and dark fixtures:
- `os validate` exits 0 with `valid: true`. Lit prints four
`flow-credential-literal` warnings: the header, the url query key, the
header inside the `try` region and the nested connector input. Dark
prints none.
- `os lint` exits 0 with `passed: true`. Lit reports the same four
warnings; dark reports none.
- The save door returns `error: null`. Lit carries one advisory per
literal on the advisory channel; dark and draft carry none.
  - No finding carries a sentinel value.
- The pins:
- `packages/lint`: the pin set is the measured scanner's
positive-control fixture (literal arm, doc-block arm, probe flow),
carried over as the evaluated stack. It draws exactly 12 advisories, and
the template, ordinary-value and `signingSecret` controls draw none.
- The save door:
`packages/metadata-protocol/src/runtime-authoring-gate.flow-credential-literal.test.ts`.
- `os validate` / `os lint`:
`packages/cli/test/flow-credential-literal-doors.e2e.test.ts`. It spawns
the CLI, so it sits in the nightly tier by name; the per-PR half is the
per-command `runAuthoringRules` pin in lint.
- Tests:
  - `@objectstack/lint`: 117 files, 5433 tests passed, typecheck exit 0.
- `@objectstack/metadata-protocol`: 190 files passed and 3 skipped (2786
tests passed, 19 skipped), typecheck exit 0.
- `@objectstack/cli`: unit project 234 files, 3355 tests passed,
typecheck exit 0. The door pin passes 2/2 under `OS_TEST_TIERS=nightly`.
The per-PR integration project is left to CI.
- Example apps: `os lint` draws 0 `flow-credential-literal` findings on
`app-showcase`, `app-crm` and `app-todo`.
- Gates:
- `dispatch-gates --commands` derives 111 gates at `ae5d8af6f8`. All 111
ran with their exit codes captured before any pipe, and all exited 0.
- `--ran` reconciliation: 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN.
- `check:type-check-debt` ran under the verify lock, because it runs its
own closure build.
- ESLint (a proven narrowing): the config's population is
`**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}`. `--format json` over the 10
changed files in it reports 0 errors and 0 warnings. The config enables
no type-aware linting, so this diff cannot move the verdict on any
untouched file.

## Acceptance notes

- **Boundaries of the rule.** It is the measured R1/R2 rule, unchanged,
and it is deliberately not widened here.
- It does not judge userinfo in a url, a secret carried in a url path
(the incoming-webhook shape), or an `http` node's `body`.
- A name like `X-Authorization` is missed by R1 unless its value opens
with an auth scheme.
- A setting under a credential-sounding name (a `session…` or `auth…`
key holding a non-secret string) draws a false positive. That costs a
line of reading, which is the ruling's trade.
- **`--strict`** on `os validate` / `os lint` promotes this warning to a
failure, exactly as it promotes every warning. No door treats it as an
error otherwise.
- **Staying in step.** The `http` descriptor's `configSchema` text in
`service-automation` is objectstack-ai#20590's face (PR objectstack-ai#20672), and it is being
tightened to the same shape routing. The landed flows-guide callout is
filed separately for its url-path over-reach, and this PR does not copy
it.
- **The template test** reuses the `http` executor's interpolation token
(a single-brace `{…}` span with no brace inside, as `template.ts`
interpolates it). The module cites it; no test pins it against that
file.

---
_Generated by [Claude
Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…#20682) (objectstack-ai#20704)

Fixes objectstack-ai#20682

Clause-②: no

Docs-only, `content/docs/automation/flows.mdx`, two spots.

1. The `http` node callout no longer sends every credential to a
declarative connector's `auth.credentialRef`. It routes by shape: a
header credential to `bearer`/`basic`/`api-key`; a query key to
`api-key` with `paramName`; a secret in the url path has no
`credentialRef` variant, so it goes to a token-authenticated connector
such as `slack`, whose bot token is supplied by host code (the Slack
connector is plugin-registered, not a declarative `connectors:`
provider; only `rest`, `openapi`, `mcp` register providers). The first
sentence and the `signingSecret` / start-node `secret` sentence are
unchanged.
2. The retired-shapes row for `actionType: 'slack'` keeps the
`connector_action` + Slack connector and drops the incoming-webhook
`http` alternative.

Wording is in step with objectstack-ai#20672's `http` descriptor and objectstack-ai#20698's
describes.

## Acceptance notes

- `pnpm --filter @objectstack/spec run check:skill-examples` (client
dist not built) and `check:gitlink-declared` were not run: NOT MEASURED.
The other derived gates ran green (33 of 40 derived; the rest are
checker self-tests), `doc-security-posture` and `docs-spec-enumerations`
included.
- No changeset: docs-only.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv

Co-authored-by: Claude <noreply@anthropic.com>
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 tests tooling

Projects

None yet

2 participants