Repository navigation
Commit 3e54f99
feat(spec)!: an authored connector entry refuses a top-level key it does not declare; the connector read withholds URL credential parts and a non-object auth (#28688)
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — every connector entry `skills/**`,
`content/docs/**`, `examples/**` and `packages/apps/**` author (each
`defineConnector({…})` and `connectors: [{…}]` literal, scanned) already
uses only declared entry keys, so nothing they teach changes; the new
refusal carries its own prescription.
## 维护者速读
- 改了什么:作者编写的连接器条目(`connectors:` / `PUT
/meta/connector/:name`)改为封闭结构,未声明的顶层键在 defineStack、os
validate/compile、构建产物加载与保存时按键名拒绝(保存为 422);连接器的元数据读取额外隐去 `providerConfig`
下 URL 的 userinfo 与凭据类 query 参数,以及非对象形态的 `auth` / `authentication`。
- 为什么改:QA 第三轮发现的安全加固(细节按披露裁决不公开)
- 风险与回滚:在连接器条目顶层多写了键的元数据会被拒(错误信息给出改法:凭据走 `auth.credentialRef`,provider
配置挪进 `providerConfig`);插件 `registerConnector` 的运行时形状不变。读出的 URL
去掉凭据部分后原样保存,存储值保留、运行时照常使用。回滚即 revert 本 PR。
-
先看:`packages/spec/src/integration/connector.zod.ts`(`DeclarativeConnectorEntryBaseSchema`
与
`connectorBaseShape`)、`packages/spec/src/integration/connector-credential-redaction.ts`(`credentialPathsOf`
的非对象分支与
`credentialUrlRewritesOf`)、`packages/spec/src/kernel/metadata-authoring-lint.test.ts`(最后一个开放根关闭,下限改为
0)。
- 两分钟验证:`pnpm --filter @objectstack/dogfood exec vitest run
test/connector-meta-door-entry-and-url-hardening.dogfood.test.ts`,预期 9
个用例全部通过。
- 需要你做的:无,席位落地
## What changes
**Write door.** An authored connector entry
(`DeclarativeConnectorEntrySchema`) is closed. It refines a dedicated
`strictObject` carrier, `DeclarativeConnectorEntryBaseSchema`, built
from the same shape declaration as the open base
(`connectorBaseShape()`, called once per carrier so each has its own
field instances and the guidance census never pairs the open `Connector`
def with the closed declaration). Every door that parses the entry
refuses a top-level key the shape does not declare, by name, with a
located `unrecognized_keys` issue: `defineStack({ connectors })`, `os
validate` / `os compile`, the metadata plugin's artifact load at boot
and reload, and `PUT /api/v1/meta/connector/:name`, where the answer is
`422 INVALID_METADATA`. Two curated families ride the refusal:
credential-shaped keys point at `auth.credentialRef`, and provider
settings written at the top level (`baseUrl`, `spec`, `transport`,
`headers`, …) point at `providerConfig`; a few near-miss spellings
(`config`, `timeout`, `retry`) are aliases to the declared key. The
ADR-0010 protection envelope stays declared, so a package-stamped entry
still parses. The refusal is terminal, so a body with an undeclared key
meets that one refusal and not the ADR-0097 refinements as well.
`ConnectorSchema`, the def a plugin hands to `registerConnector` and the
shape `defineConnector()` parses, keeps its posture; a control pin says
so.
**Read redaction.** The built-in `connector` redactor withholds two more
positions:
- every URL at an object depth of `providerConfig` (a string starting
with `scheme://`) is served without its userinfo and without its
credential query parameters (the datasource names plus the HTTP
conventions such as `api_key`, `access_token`, `token`, `key`, `sig`,
and the `auth.paramName` an `api-key` instance names), the rest of the
URL byte for byte. That covers `baseUrl`, `transport.url` and a URL
`spec`. The query half goes through the same
`redactUrlCredentialQueryParams` the datasource redactor uses, which now
takes an optional name list (default and existing callers unchanged).
- an `auth` or `authentication` value that is not an object is withheld
whole. Cleared forms (`null`, `''`) are served as written.
Each rewritten or withheld position is named in `redactedKeys`, so the
protocol's carry-forward keeps the stored value on a save of the served
form, and the runtime keeps reading the unredacted execution view.
**ADR-0087.** A D3 semantic entry,
`connector-entry-undeclared-key-refused`, carries the prescription;
there is no mechanical rewrite, because an undeclared key may be a
provider setting whose home depends on the provider, or the only copy of
a secret.
**Ledgers.** The strictness ledger's `integration/` row is re-classed
from a flat `wire` to `mixed`, with the reason and the governing texts
read before the close (ADR-0097, ADR-0143, ADR-0104, the type-schema
suite's #2657 fence; none keeps the authored entry open). The authoring
lint's last open root was `connectors`: its coverage floor moves to 0,
asserted deliberately with the departed root confirmed rejected by the
parse, and its two cross-collection fixtures move to a nested strip site
(`connectors[].retryConfig`).
## Tests
-
`packages/spec/src/integration/connector-entry-undeclared-keys.test.ts`
(new, 12 cases): the refusal for a scalar, a nested container and two
keys on an instance and on a descriptor, pinned by `code`, `path` and
`keys`, never echoing the value; both prescriptions; terminality; the
stamped envelope; a retired key keeping its own prescription and the
retired-default residue; the stack door; and the open `ConnectorSchema`
as a control.
- `packages/spec/src/integration/connector-credential-redaction.test.ts`
(+15 cases): non-object `auth` / `authentication` withheld whole,
cleared forms served; userinfo, a bare user-part token, case-insensitive
query parameters, `transport.url`, a URL `spec`, a relative `spec` left
alone, the instance's own `paramName`, untouched non-credential URLs,
arrays and dot-spelled keys off the walk, and a withheld map not walked
twice.
-
`packages/qa/dogfood/test/connector-meta-door-entry-and-url-hardening.dogfood.test.ts`
(new, 9 cases, showcase boot): WRITE refusals (`422 INVALID_METADATA`,
the key named, nothing stored, `404` to a member); CONTROL
(declared-keys-only instance saves, reaches `ready`, dispatches with the
reference resolved); READ for a member and an administrator on the item,
layered and list reads (both spellings), with the stored rows untouched;
CARRY-FORWARD (a save of the served form keeps the stored URL whole, and
dispatch sends it).
- `packages/spec/src/kernel/metadata-authoring-lint.test.ts`: the
graduation above.
Ablation, one leg per fix, each with on-disk mutation proof and restore
proof (`scripts/ablation-replace.mjs`: anchor count, blob change, blob
back to HEAD, empty `git diff HEAD`), and for the dogfood legs a spec
rebuild with `scripts/ablation-dist-preflight.mjs` proving the marker
reached `dist/` and was gone after the restore rebuild:
| Fix ablated | Spec unit (src) | Dogfood (dist) |
|---|---|---|
| entry back on the open base | 7 of 12 red (every refusal; controls
green) | 4 of 9 red (3 WRITE; the list then serves the stored keys) |
| URL rewrites never applied | 5 of 32 red (every URL rewrite case) | 3
of 9 red (item and layered read, list, carry-forward) |
| non-object `auth` / `authentication` not withheld | 4 of 32 red (the
four non-object cases) | 2 of 9 red (item and layered read, list) |
A first combined dist leg is not counted: its URL-rewrite marker was
constant-folded by the bundler and the preflight could not find it in
`dist/`, so that leg was redone per fix with a string-literal marker.
## Acceptance notes
- The `422` envelope locates an unknown top-level key at the entry root
(`path: ''`) and names it in the message, as zod locates
`unrecognized_keys` at the containing object on every closed type.
- An artifact `os build` wrote carries no undeclared key (it serializes
the parsed stack), so only a hand-assembled artifact can meet the
refusal at boot.
- `metadata-type-schemas.test.ts`'s two notes that described the
connector as open are dated rather than deleted; the suite's fence
(envelope only, no posture case) is unchanged.
## Verification
Local runs on head `406ef4017` (merge of `origin/main` at `6b9c1ca99`),
each under the shared verify lock, verdict lines quoted:
<details><summary>Package suites (all green)</summary>
| Command | Result | Lock verdict |
|---|---|---|
| `pnpm --filter @objectstack/spec test` | 783 files, 23149 passed, 1
todo | `command-exit 0` |
| `pnpm --filter @objectstack/metadata-protocol test` | 291 passed, 4
skipped files; 21944 passed, 26 skipped | `command-exit 0` |
| `pnpm --filter @objectstack/metadata test` | 65 files, 918 passed |
`command-exit 0` |
| `pnpm --filter @objectstack/service-automation test` | 251 files, 3185
passed | `command-exit 0` |
| `pnpm --filter @objectstack/objectql test` | 512 files, 9898 passed |
`command-exit 0` |
| `pnpm --filter @objectstack/runtime test` | 503 files, 8171 passed, 19
skipped | `command-exit 0` |
| `pnpm --filter @objectstack/service-datasource test` | 67 files, 1125
passed | `command-exit 0` (first attempt `99`, never acquired; second
attempt ran) |
| `pnpm --filter @objectstack/cli exec vitest run --project unit` | 363
files, 5162 passed | `command-exit 0` (the `integration` tier is
declared to CI: the diff touches no spawn entry or integration file) |
| `pnpm --filter @objectstack/dogfood typecheck` | `tsc --noEmit` clean
| `command-exit 0` |
| `pnpm --filter @objectstack/dogfood exec vitest run
test/connector-meta-door-entry-and-url-hardening.dogfood.test.ts
test/connector-meta-door-credential-hardening.dogfood.test.ts` | 2
files, 20 passed | `command-exit 0` (pre-merge head `14e25f5de`; the
full battery below reruns both on this head) |
The connector packages (`connector-rest`, `connector-openapi`,
`connector-mcp`) are not in the diff and consume `ConnectorSchema`,
which does not move; their suites are left to CI.
</details>
Head is now `91b48bc4b`, one commit after `406ef4017`. That commit is
test(cli): it moves the #22238 probe key in
`packages/cli/test/package-union-readers.test.ts` to a nested strip site
(`retryConfig`), because the connector root is now closed. CI Test Core
(6/8) on `91b48bc4b` ran that file: 7 passed, cli 137 files / 1718
passed.
- `pnpm --filter @objectstack/spec typecheck`: NOT MEASURED locally. Six
attempts on the contended verify lock all answered `99`. CI `TypeScript
Type Check` and `Type Check · workspace` are success on `91b48bc4b`.
- The full dogfood battery `pnpm --filter @objectstack/dogfood test` on
`91b48bc4b` (the `!` title owes it): `os-verify-lock` VERDICT
command-exit 0, Test Files 370 passed | 3 skipped (373), Tests 2990
passed | 28 skipped (3018). CI's Dogfood Regression Gate skips the
dogfood legs on `pull_request` by design, so this local battery is the
evidence.
- CI check-runs on `91b48bc4b`: 56 success, 9 skipped, 0 failure.
Derived gate families (`node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands`, merge base `004871f79`): 85
derived, 84 run with exit 0, 1 NOT MEASURED (`pnpm
check:dual-build-cjs-loads`, exit 3, PREREQUISITE NOT MET: seven
packages outside this diff have no `dist/` in this worktree); `--ran`
reconciliation: 85 accounted, 0 unrun. `pnpm --filter @objectstack/spec
check:generated` is among the 84 and is green after
`gen:strictness-ledger` (the `integration/` shard total moves 3 → 4).
Every family CI runs is read from this PR's check-runs, not re-run here.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 447a2a6 commit 3e54f99
13 files changed
Lines changed: 854 additions & 41 deletions
File tree
- .changeset
- docs/audits
- 2026-07-unknown-key-strictness-ledger.counts
- packages
- cli/test
- qa/dogfood/test
- spec/src
- data
- integration
- kernel
- migrations/entries/semantic
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1376 | 1376 | | |
1377 | 1377 | | |
1378 | 1378 | | |
1379 | | - | |
| 1379 | + | |
1380 | 1380 | | |
1381 | 1381 | | |
1382 | 1382 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
98 | | - | |
| 98 | + | |
99 | 99 | | |
100 | 100 | | |
101 | 101 | | |
| |||
121 | 121 | | |
122 | 122 | | |
123 | 123 | | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
124 | 127 | | |
125 | 128 | | |
126 | 129 | | |
| |||
259 | 262 | | |
260 | 263 | | |
261 | 264 | | |
262 | | - | |
| 265 | + | |
263 | 266 | | |
264 | 267 | | |
0 commit comments