Skip to content

Commit 3e54f99

Browse files
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
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
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`
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: registered connector-entry-undeclared-key-refused -->
11+
12+
**BREAKING** accept-set narrowing on an authored connector entry (`DeclarativeConnectorEntrySchema`), at every door that parses it: `defineStack({ connectors })`, `os validate` / `os compile`, the metadata plugin's artifact load at boot and on reload, and the metadata save door `PUT /api/v1/meta/connector/:name`.
13+
14+
- **Before.** The entry was a plain object: a top-level key it does not declare parsed green and meant nothing to the runtime.
15+
- **Now.** The entry is closed. Such a key is refused by name with a located error (`unrecognized_keys` at the entry; at the save door `422 INVALID_METADATA`). A credential-shaped key (`apiKey`, `token`, `secret`, `password`, …) meets the `auth.credentialRef` prescription, and a provider setting written at the top level (`baseUrl`, `spec`, `transport`, `headers`, …) is pointed at `providerConfig`. The ADR-0010 protection envelope stays declared, so a package-stamped entry still parses. 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.
16+
- **Unchanged.** `ConnectorSchema` — the def a plugin hands to `registerConnector`, and the shape `defineConnector()` parses — keeps its posture. Nothing in the runtime ever read an undeclared entry key, so no connector's behaviour moves.
17+
- **Read half.** The built-in `connector` redactor also withholds, on every read exit that serves a stored body through the metadata-type redactor registry: the userinfo and the credential query parameters of every URL under `providerConfig` (`baseUrl`, `transport.url`, a URL `spec`, and any other URL a provider keeps at object depth), the rest of each URL served byte for byte; and an `auth` or `authentication` value that is not an object, whole. A save of the served form keeps the stored value (the metadata protocol's carry-forward), and the runtime keeps reading the unredacted execution view.
18+
- `redactUrlCredentialQueryParams` (`@objectstack/spec/data`) takes an optional parameter-name list; its default, and every existing caller, is unchanged.
19+
20+
## FROM → TO
21+
22+
```ts
23+
// before — parsed green; neither key was read by anything
24+
{ name: 'billing_api', label: 'Billing API', type: 'api', provider: 'rest',
25+
baseUrl: 'https://billing.example.com', apiKey: '…',
26+
auth: { type: 'bearer', credentialRef: 'BILLING_API_TOKEN' } },
27+
28+
// after — refused by name; the provider setting moves under providerConfig, the secret moves out of metadata
29+
{ name: 'billing_api', label: 'Billing API', type: 'api', provider: 'rest',
30+
providerConfig: { baseUrl: 'https://billing.example.com' },
31+
auth: { type: 'bearer', credentialRef: 'BILLING_API_TOKEN' } },
32+
```
33+
34+
Set the secret under the name `credentialRef` gives (an environment variable in the open tier, or Setup → Connector credentials) and delete the key. There is deliberately no automatic rewrite: an undeclared key may be a provider setting whose home depends on the provider, or the only copy of a secret. The upgrade guide entry is `connector-entry-undeclared-key-refused`.

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/integration.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate.
1919

2020
| Dir | Sites |
2121
|---|---|
22-
| `integration/` | 3 |
22+
| `integration/` | 4 |

‎docs/audits/2026-07-unknown-key-strictness-ledger.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1376,7 +1376,7 @@ rest at #5107.
13761376
| `kernel/` | **mixed · `manifest.zod.ts` authorable, the rest wire** | ⚠️ **Split at #14192 — the flat `wire` verdict this row carried was right for the directory and wrong for one file in it, the `api/` row's finding one directory over.** `kernel/manifest.zod.ts` is `manifest:` — the hand-authored identity block of every `objectstack.config.ts`, every `os init` template, every example app, the Studio package form and the `objectstack.plugin.json` that `os plugin build` reads — and it was a plain open `z.object` under a cell that read "code-to-code". Measured first (2026-09-02 at `bd4aa4e49`, from `src` and through the built `dist`, identical readings): an unknown key parsed green and was DROPPED at all seven doors that reach `ManifestSchema` — the stack `manifest:` member (`defineStack` / `os validate` / `os compile`), `devPlugins[]`, the artifact `packages[]` entry in both its authoring and assembled forms, `os plugin build`, the `InstalledPackage` row schema, and the six package request/snapshot wire shapes that embed it. A transposed `namesapce` left `manifest.namespace` undefined at every one of them with exit 0 — and that key decides table names, REST paths and the install-time namespace gate (ADR-0129 D1–D2, ADR-0048 §3.2, ADR-0130 D1/D3). The same silent drop held one level down for `contributes` (`kind:` for `kinds:`), `contributes.kinds[]` and `engines` (`protocl:` for `protocol:`, which switches the load-time protocol handshake off). **The order the `api/` row records was kept: measurement, then the close.** No stored-row caller feeds this schema — `registry.validate('package' \| 'plugin')` has no live caller (`installPackage` stores the caller's object verbatim, `registerPlugin` is uncalled), the durable `sys_packages` row is re-registered through `registerApp` without a parse, and no bookkeeping key (`packageId` / `state` / `published*` / `package`) reaches any door — so the `peelStoredEnvelope` debt the `api/` row had to pay first does not exist here, and no bookkeeping key was taught to the authoring schema. Exactly ONE undeclared key was found at a real door, and it was DECLARED rather than curated: `main`, which `os plugin build` reads off the source `objectstack.plugin.json` to pick its entry and writes back into the compiled manifest as `dist/index.mjs`; three CLI fixtures author it, and a naked `strictObject` would have refused every plugin that names its entry — the shape this row would otherwise have recorded as refused. `ManifestSchema`, `contributes`, `contributes.kinds[]`, `engine` and `PluginEnginesSchema` are `strictObject` now: an undeclared key is a named rejection carrying the surface, the key and a rename (`namesapce` → `namespace`, `protocl` → `protocol`); the retired keys keep their `retiredKey()` tombstones (a tombstone types the key `never` and carries the upgrade text, which a bare refusal would not, and `acceptsNothing` keeps them out of the rename candidates); one curated wrong-layer pointer ships, `specVersion` (the retired CLI advisory axis — the live one is `engines.protocol`). `AssembledPackageBodySchema` (`stack.zod.ts`) inherits the closed posture through `.extend()`, so the load gate refuses an undeclared key on an assembled body too; that file's docblock sentence "NOT `strictObject`: `ManifestSchema` is an open object" is stale as of this change and is reported for its owner rather than edited here. Blast radius, measured with a strict preview before the close: 0 undeclared keys across the four example apps, the three `os init` templates, the `create-objectstack` blank template, the downstream-contract stack, the empty-kernel host and the i18n service manifest; the only shipped-block failure — `packages/cli/src/commands/create.ts`'s template manifest — fails the OPEN schema too (it lacks `id` and `type`) and is reported separately. Cloud is NOT MEASURED for this file (no checkout in the container, code search refused); if cloud parses a published `.osplugin` manifest through this schema, the compiled manifest it receives carries only declared keys (`main` and `integrity` included) by construction of `os plugin build`. The rest of `kernel/` is unchanged: plugin/kernel contracts, code-to-code |
13771377
| `cloud/` | wire | multi-tenant runtime |
13781378
| `ai/` | mixed | agent/tool/skill definitions authored (partially strict already); model/provider payloads wire |
1379-
| `integration/` | wire | connector payloads — upstream adds fields freely |
1379+
| `integration/` | **mixed · `connector.zod.ts`'s declarative entry authorable, the rest wire** | ⚠️ **Split at the connector-entry close — the flat `wire` verdict this row carried was right for the payloads and wrong for one shape, the `api/` and `kernel/` rows' finding again.** `DeclarativeConnectorEntrySchema` is the `connectors:` stack collection and the `connector` row the metadata save door keeps (`PUT /api/v1/meta/connector/:name`, bound in `UNREGISTERED_KIND_SCHEMAS`, which stores the request body for every type but `view` — ADR-0005 appendix (c), ruling B): an AUTHORED, STORED, SERVED type, not an upstream payload, yet it was a plain `z.object`, so an undeclared top-level key parsed green at every door that reads it. It is closed through a dedicated `strictObject` carrier (`DeclarativeConnectorEntryBaseSchema`) built from the same shape factory as the open one, each with its own field instances so `computeGuidanceRoutes()` never pairs the open `Connector` def with the closed declaration's `guidance`. Every door that parses the entry — `defineStack({ connectors })`, `os validate` / `os compile`, the artifact load at boot and reload, and the save door (`422 INVALID_METADATA`) — refuses the key by name, located at it; two curated families ride the refusal: credential-shaped keys → `auth.credentialRef`, provider settings → `providerConfig`. The ADR-0010 envelope was already declared on the shape (commit b5404f496), so no stamped entry is refused. Governing texts read before the close: ADR-0097, ADR-0143, ADR-0104 and the type-schema suite's #2657 fence — none keeps the authored entry open; the `ConnectorSchema is NOT .strict()` sentences across `connector.zod.ts` state the fact that made each retirement a `retiredKey()` tombstone, not a decision to stay open. ⛔ Deliberately NOT closed: `ConnectorSchema`, the def a plugin hands to `registerConnector` (code-to-code — the wire half of this row) and the shape `defineConnector()` parses, so `defineConnector({ … })` still drops an undeclared key before the entry sees it; closing that carrier is its own decision. The rest of `integration/` is unchanged: connector payloads, upstream adds fields freely |
13801380
| `identity/` | mixed | position/user shapes authored (`PositionSchema` **strict as of #4001 step 2**, with the ADR-0010 envelope declared); auth payloads wire. **34 → 33 in #4641**: `identity.zod.ts` lost its `SessionSchema` site — a second, importerless declaration of a name `api/auth.zod.ts` already owned (the #4411 dual-source trap), deleted rather than reclassified |
13811381
| `shared/` | **mixed · `protection.zod.ts` and `http.zod.ts`'s rate-limit budget authorable, the rest as written** | ⚠️ **Annotated again at #18578 — a FOURTH instance of the same shape, and this one was invisible to a sweep that went looking for it.** `shared/http.zod.ts`'s `RateLimitConfigSchema` is a building block with two mounts, and this row's rationale — *strictness decided at the consuming schema* — held for exactly one of them: `system/stack-server.zod.ts` re-postured it closed, and it did so by building `strictObject(…, RateLimitConfigSchema.shape)` **from this shape object**, so ONE declaration answered for TWO emitted defs with OPPOSITE doors. `api/endpoint.zod.ts` mounts the same schema BARE on `apis[].rateLimit` — a registered metadata type since #5312, authored through `defineStack({ apis })`, the Studio form and `PUT /meta/api/:name` — where nothing re-postures it, so the two `guidance` entries (`keyBy`, `store`) prescribed to nobody and a misspelled budget was worse: `windowSeconds: 60` parsed green and metered the 60000 ms default, a thousandfold miss reported as success. ⚠️ **The sweep that should have caught it could not.** A `CONTRACT_REVIEW_TIER` review swept this exact class with live controls (5 `.strip()` sites, 0 `z.object(XSchema.shape…)`, 0 `z.object(bareIdentifier)`) and found nothing, because the sharing runs the other way round here: it hunted an OPEN clone built from a STRICT schema's shape, and this is the STRICT one built from the OPEN one's shape — every grep shape in that sweep is blind to that direction by construction. It was found instead by driving the gate's own instrument (`computeGuidanceRoutes()` in `scripts/build-schemas.ts`) over every emitted def: match each def to its declaration, then ask the def what it actually does with the key. Census on `42f8df1723`: 1527 emitted defs, 258 resolving to exactly one declaration, 779 keys promised, **770 delivered, 9 not** — 2 of them this live silent strip, the other 7 union defs whose discriminator the probe's one-key document cannot supply (they DO deliver to an author who writes a whole document). The close moves the strictness and both tables onto the shared schema, leaving ONE declaration and ONE door for both defs: 772 delivered, 7 not. ⚠️ The residual 7 are a PROBE boundary, ⛔ not a clean zero and ⛔ not a finding. ⚠️ **Annotated at #16845 — the flat `n/a` verdict this row carried was right for the directory and wrong for one file in it, the `api/` and `kernel/` rows' finding a third time.** — the flat `n/a` verdict this row carried was right for the directory and wrong for one file in it, the `api/` and `kernel/` rows' finding a third time.** ⛔ **This directory is still UNTRIAGED and this is not a per-file row** — `shared/` has never had per-file rows, so `shared/protection.zod.ts` was **never enumerated** by this campaign; it was not deliberately accepted, because no verdict was ever taken on it. Two measurements say why the silence carried no information. **①** This row's own rationale — *strictness decided at the consuming schema* — is false for this file: all **16** `protection: ProtectionSchema` mounts across **14** files are the bare `ProtectionSchema.optional()`, not one `.extend()` / `.merge()` / `.omit()` / re-posture among them, so both the strictness and the MESSAGE are decided here in `shared/` and inherited verbatim by objects, views, dashboards, datasets, reports, apps, flows, webhooks, permissions, positions, email templates, agents, tools and skills — very nearly every authorable metadata type in the platform. A row is read as licence, which is what this ledger's own gate exists to prevent. **②** Even a per-file row would not have surfaced the defect, and this is the part worth keeping: `ProtectionSchema` has been `.strict()` since it was introduced, so it counts in the `strict` column and could never carry a remaining-strip row. **This ratchet's axis is CLOSURE; the defect was MESSAGE QUALITY** — a closed shape with no error map, refusing `lockk` with zod's bare `Unrecognized key: "lockk"`: no surface, no declared-key list, no rename, on every one of those mounts. `strictObject` at #16845 (the `PluginPermissionsSchema` conversion at #16328 one directory over is the precedent). ⛔ The population of *closed shapes still carrying zod's bare message* is **NOT MEASURED** here and is not this row's claim — #14722's rule stands that a sweep needs its own card with its own measured count. The rest of `shared/` is unchanged: utilities and building blocks whose strictness really is decided at the consuming schema |
13821382
| `qa/` | n/a | test fixtures |

‎packages/cli/test/package-union-readers.test.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const app = { name: 'unr_app', label: 'Union', navigation: [{ id: 'nav_tickets',
9595
const ticketZh = { 'zh-CN': { objects: { unr_ticket: { label: '工单', pluralLabel: '工单', fields: { title: { label: '标题' } } } },
9696
apps: { unr_app: { label: '联合', navigation: { nav_tickets: { label: '工单' } } } } } };
9797
const noteZh = { 'zh-CN': { objects: { unr_note: { label: '笔记', fields: { name: { label: '名称' } } } } } };
98-
const conn = { name: 'unr_conn', label: 'Conn', type: 'api', zzzUnionProbeKey: 1 };
98+
const conn = { name: 'unr_conn', label: 'Conn', type: 'api', retryConfig: { zzzUnionProbeKey: 1 } };
9999
`;
100100

101101
const CONFIGS: Record<string, string> = {
@@ -121,6 +121,9 @@ export default defineStack({ manifest: appManifest, objects: [ticket, note], app
121121
`,
122122
// An undeclared connector key. \`strict: false\` keeps it past the producer's
123123
// parse, so the doors' own pre-parse walk is the only carrier into \`--json\`.
124+
// It sits one level down, in \`retryConfig\` (a strip-mode shape the walk still
125+
// visits): the connector entry's own root is closed, so a top-level key there
126+
// is refused by the artifact load rather than warned about.
124127
keyTwo: `${PIECES}
125128
const svc = defineStack({ manifest: svcManifest, objects: [note], connectors: [conn] } as any, { strict: false });
126129
const appStack = defineStack({ manifest: appManifest, objects: [ticket], apps: [app] } as any);
@@ -259,6 +262,6 @@ describe('#22238 — the undeclared-authoring-key walk reads the package union',
259262
it('`os validate --json` carries the finding in `warnings` for a two-package app', () => {
260263
const run = r.validateTwo;
261264
expect(run.code, `${run.stdout}\n${run.stderr}`).toBe(0);
262-
expect(named(run)).toEqual([expect.stringContaining('connectors.unr_conn.zzzUnionProbeKey')]);
265+
expect(named(run)).toEqual([expect.stringContaining('connectors.unr_conn.retryConfig.zzzUnionProbeKey')]);
263266
});
264267
});

0 commit comments

Comments
 (0)