Skip to content

Commit 45b4922

Browse files
feat(spec): the package export manifest declares withheldMaps, classified door for the unmanaged import (#29521)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi93WJttNHMw9kPDPtpWDpXEq)_ Fixes #29373 Clause-②: yes (widening) Metadata-protocol: added Skills-docs: none — an export response key the runtime writes; no metadata author writes it, so no skill or authoring doc teaches it ## 维护者速读 - 改了什么:包导出清单(`GET /packages/:id/export` 的契约)新增可选键 `withheldMaps`,逐条列出导出时被整张扣下的连接器请求头 / 环境变量表的位置(只列位置,不列任何头名或变量名);非托管导入的键分类表把它记为 `door`。导出一直声明的 `label` 在该表里也从未分类,同 PR 一并记为 `door`。 - 为什么改:导出读的是脱敏后的条目,连接器的四张凭据表在导出里直接缺席,导入方分不清"被扣下"还是"本来就没有"。这是 #28640 裁决 ③A 的 spec 半边,server 半边 #29342 等它落地后再写导出和两道安装门。 - 风险与回滚:纯 spec 声明加一行分类,无运行时改动,导出暂不写该键。实测:改动前,带 `label` 或 `withheldMaps` 的导出被非托管导入整包拒收;改动后两者都被接受、新基座清单不带它们。回滚即 revert 本 PR。 - 先看:`packages/spec/src/api/package-lifecycle.zod.ts`(新键与 describe)、`packages/spec/src/kernel/unmanaged-install.ts`(两行 `door` 及注释)、`packages/spec/src/kernel/unmanaged-install.test.ts`(全量性测试改为同时读导出清单的固定键)。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/api/package-lifecycle.test.ts src/kernel/unmanaged-install.test.ts`,预期 2 个文件全部通过,含「refuses a list of the withheld header names」与「classifies every fixed key of the package export」两条新用例。 - 需要你做的:无,席位落地。 ## New names (proposed; the seat's call) - `PackageExportManifestSchema.withheldMaps`: a new fixed key on a published contract. - Its entry's sub-keys: `type` (the literal `connector`), `name`, `path` (closed set: `providerConfig.headers`, `providerConfig.defaultHeaders`, `providerConfig.transport.headers`, `providerConfig.transport.env`). - No new export: the entry is declared inline, so `api-surface` does not move. - Alternatives considered: `withheld` (shorter, but reads as a boolean), `incompleteMaps` (names the import's reading rather than the export's act), `redactedMaps` (collides with the redactor's internal `redactedKeys`, which is deliberately never put on the wire). ## What changes - `packages/spec/src/api/package-lifecycle.zod.ts`: `PackageExportManifestSchema` gains the optional fixed key `withheldMaps`, an array of strict `{ type: 'connector', name, path }` entries. The docblock now says five fixed keys and explains the key (#28640 ruling ③A). `PackageExportManifestSchema` stays the same exported constant, so the bundle schema in draft #29100 composes it by identity as before. - `packages/spec/src/kernel/unmanaged-install.ts`: `UNMANAGED_INSTALL_KEY_DISPOSITIONS` gains `withheldMaps: 'door'` and `label: 'door'`. The module note and the table's docblock now name the export's fixed keys as the third schema a package body is read with; the `door` definition says the door may set a key aside (the export's `label`). - Regenerated: `content/docs/references/api/package-lifecycle.mdx` (the key and its nested shape), `packages/spec/authorable-surface/api.json` (one row: `api/PackageExportManifest:withheldMaps`), `docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md` (`api/` sites 487 to 488, the new `.strict()` entry). - `.changeset/29373-export-withheld-maps.md`: `@objectstack/spec` minor, with the two declaration lines copied from the claim as the seat amended it (`Metadata-protocol: added`). - Not touched: runtime. The export does not write the key yet, and the install doors do not read it yet; that is the server half, #29342. ### The path vocabulary, measured The card spells three of the four maps without their root (`defaultHeaders`, `transport.headers`, `transport.env`). On main the connector redactor's `CONNECTOR_CREDENTIAL_MAP_PATHS` (`packages/spec/src/integration/connector-credential-redaction.ts`) holds all four under `providerConfig`, and `redactConnectorItem` reports them in `redactedKeys` as `providerConfig.headers`, `providerConfig.defaultHeaders`, `providerConfig.transport.headers`, `providerConfig.transport.env`. The closed enum uses that spelling, so the export can copy the redactor's own report. A closed set of four is right for this card: it is exactly the set the redactor withholds whole as maps. The redactor's other positions (undeclared `auth` keys, `authentication` keys, credential URL parts) are single values, not maps, and webhook `headers` is out of scope by the seat ruling. A test holds the enum equal to the redactor's list and to its `redactedKeys` on a real redaction, so the two cannot drift. ### `label`, measured The card noted (not measured) that the export's top-level `label` is not classified. Measured on this branch's base (`04e0c2c0`): `PackageExportManifestSchema` declares `label`; `UNMANAGED_INSTALL_KEY_DISPOSITIONS` has no row for it; the import planner's default arm refuses an unclassified key. A probe calling `planUnmanagedImport` with `{ id, name, version, label: 'Example CRM' }`, run against a spec build with the two rows removed, answered `UNMANAGED_INSTALL_UNCOPYABLE` "`label` — not a key a package body declares, so the import cannot place it" (and the same for `withheldMaps`); against this PR's build both are accepted and the base manifest is `{ name, version, id, namespace }`. So it is a real gap, classified here and declared as a deviation. It is `door`, not `identity`: the base is installed through the closed `PackageInstallManifestSchema`, which has no `label`, so carrying it would refuse the base. Reach: the export writes `label` only from `pkg.manifest.label ?? pkg.label`, and no schema in this tree declares either (`ManifestSchema` and `PackageInstallManifestSchema` are closed without `label`), so no declared producer here emits it today; the gap is between two spec contracts. ## Tests New: - `package-lifecycle.test.ts`: an export carrying `withheldMaps` parses and is preserved; the key is optional; the path enum equals `CONNECTOR_CREDENTIAL_MAP_PATHS` and the redactor's `redactedKeys`; five refusals (a position outside the four, a position without its `providerConfig` root, a non-connector type, an entry carrying a list of header names, an entry with no name). - `unmanaged-install.test.ts`: "classifies every fixed key of the package export", and a pin that `withheldMaps` and `label` are `door`. Forced edits (named): - `unmanaged-install.test.ts` "classifies nothing neither schema declares": renamed "classifies nothing no schema declares", and its declared set now includes the export's fixed keys. Without this, the new `withheldMaps` row would red it as a stale row. - `unmanaged-install.test.ts` and `package-lifecycle.test.ts` describe titles: "both schemas" to "the three schemas"; "four fixed keys" to "five fixed keys"; "requires exactly the four fixed keys" to "requires only the three identity keys" (the old title was already loose: `label` was optional). Reverse check (each table row deleted with `ablation-replace.mjs`, then `unmanaged-install.test.ts`): deleting `withheldMaps: 'door'` reds "classifies every fixed key of the package export" and the door pin; deleting `label: 'door'` reds the same two. Tree restored to HEAD each time (`git diff HEAD` empty). ## Acceptance notes - `Check Changeset` level axis: the claim first read `Metadata-protocol: none`; the gate refuses it because the regenerated `packages/spec/authorable-surface/api.json` shard moved, and refuses `added` with a `patch` changeset. The seat amended the claim to `added`, matching main's precedent for API response keys (`.changeset/28737-record-page-timeline-count.md`, `28722-customizations-contributions.md`), and the changeset is now `@objectstack/spec: minor` (commit `27e2a7a5`). Measured offline on head `27e2a7a5` with this body: `check-changeset-no-major.mjs --base origin/main --event FILE` exits 0 (LEVEL AXIS passes). - Opened on base `04e0c2c0`, not merged with main: main is 20 commits ahead, none touching these 8 files or the `api` shards they regenerate (`git diff HEAD...origin/main` on spec touches ui/contracts/conversions/migrations only), and `git merge-tree` is clean. Whichever of this PR and #29100 lands second merges main. - Webhook `headers` (the same whole-map case) stays out of scope by the seat ruling on #29342; widening `type` to a union later is a widening. ## Verification Readings on head `bb1718cc` (base `04e0c2c0`), all under `os-verify-lock.sh` (VERDICT command-exit 0 each). Head `27e2a7a5` adds one commit that edits only the changeset's level and declaration line (no package source, no generated artifact), so these readings carry to it; the changeset gate was re-run on `27e2a7a5`. <details><summary>Spec and consumer suites</summary> - `turbo run build --filter='./packages/**'`: 78/78 tasks, 0 cached. - `pnpm --filter @objectstack/spec test`: Test Files 792 passed (792), Tests 23529 passed, 1 todo. - `pnpm --filter @objectstack/spec typecheck`: exit 0. - `pnpm --filter @objectstack/spec check:generated`: 2 of 14 stale before `--fix` (`check:docs`, `check:strictness-ledger`), both regenerated and committed; `api-surface` not stale (no new export). - `@objectstack/metadata-protocol` `package-unmanaged-install.test.ts` + `protocol.duplicate-package-install-refusal.test.ts`: 2 files, 68 tests passed. - `pnpm --filter @objectstack/client test`: 54 files, 693 tests passed; `typecheck` exit 0 (`PackageExportManifest` is the client's `packages.export` return type). - `@objectstack/runtime` `packages-export-override-declaration`, `packages-export-per-package-layer`, `packages-seed-apply-read-decorations`, `packages-vetted-org-source`: 4 files, 114 tests passed. - `@objectstack/cloud-connection` `marketplace-install-local-install-modes.test.ts`: 23 tests passed. - Planner probe (temporary test file, deleted): before (rows removed, spec rebuilt, dist preflight `--absent` passed) `label` and `withheldMaps` each refused `UNMANAGED_INSTALL_UNCOPYABLE`; after, both accepted. </details> <details><summary>Derived gates (dispatch-gates --commands)</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 103 families at `bb1718cc`; each ran with stdin closed, all 103 exit 0. Reconciliation: "103 derived, 103 run, 0 NOT-MEASURED, 0 UNRUN" (a DERIVED zero). Changed lines 161 (+144 / -17) against the 3000 threshold. </details> <!-- signed: domain:spec#1 · os-steve · session_01ADHkmPi3pqv7Vkx4kymD87 --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7fc4d23 commit 45b4922

8 files changed

Lines changed: 182 additions & 18 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): the package export manifest declares `withheldMaps`, the maps it withheld whole, and an unmanaged import accepts it
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: added
9+
10+
`GET /packages/:id/export` writes each item as a served read returns it, so a connector's header and environment-variable maps are already withheld whole by the connector redactor and the exported item simply lacks them. An import elsewhere could not tell such a map from one the package never had. The export contract now has a place to say so:
11+
12+
- **`PackageExportManifestSchema` gains an optional fixed key `withheldMaps`**, a list of `{ type, name, path }`: `type` is `connector`, `name` is the connector item's name, and `path` is one of the four positions the redactor withholds whole, spelled as it reports them: `providerConfig.headers`, `providerConfig.defaultHeaders`, `providerConfig.transport.headers`, `providerConfig.transport.env`. Positions only: no header or variable name is listed, and an entry carrying any other key is refused. It is declared ahead of its producer: no export writes it and no install door reads it yet, and its describe opens `[PLANNED — not yet served]` until the runtime half serves it. Until then, an export's lack of the key says nothing about what was withheld.
13+
- **`UNMANAGED_INSTALL_KEY_DISPOSITIONS` classifies `withheldMaps` as `door`**, so an unmanaged import accepts an export that carries it instead of refusing the whole package for an unrecognised key. The runtime half has the door read it to name each incomplete map; the new base never carries it.
14+
- **The export's `label` is classified `door` too.** The export contract has always declared it, but the table never classified it, so an unmanaged import refused any export that carried one. The base's install manifest declares no `label`, so the base is named by the request's `targetName` or the source `name`, as before.
15+
16+
The table's totality test now reads the export's fixed keys as a third schema a package body is read with, beside `ManifestSchema` and the stack definition, so a fixed key the export gains reddens a spec test on the PR that adds it.

‎content/docs/references/api/package-lifecycle.mdx‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ declared a VERSION rollback against the live COMMIT-rollback path;
4343
`RollbackToPackageCommitResponseSchema` below is the true contract,
4444
authored after that retirement per the ruling's sequencing (3A).
4545

46+
Two members are not transcriptions but DECLARED AHEAD OF THEIR PRODUCER
47+
(ADR-0049), each describe opening `[PLANNED — not yet served]` until the
48+
change that serves it drops the marker: the `nav` arm of
49+
`GetPackageCustomizationsResponseSchema.contributions` (#27928) and
50+
`PackageExportManifestSchema.withheldMaps` (#28640 ruling ③A, served by
51+
#29342). Their own docblocks say what is not served yet.
52+
4653
<Callout type="info">
4754
**Source:** `packages/spec/src/api/package-lifecycle.zod.ts`
4855
</Callout>
@@ -316,7 +323,7 @@ A field or flow another package contributes to one of the base's objects.
316323

317324
| Property | Type | Required | Description |
318325
| :--- | :--- | :--- | :--- |
319-
| **packages** | `({ id: string; name: string; version: string; label?: string } & Record<string, any>)[]` | ✅ | Every package whose registry row's `source` is not `code` (the marketplace and authored packages, a `.custom` pair among them), enabled or not, each once and each as `GET /packages/:id/export` answers it, dependencies first: an entry comes after every entry its package's `manifest.dependencies` names, so a `.custom` pair comes after its base. Install the entries in this order; never re-sort them. No entry carries an enabled state, so every entry installs enabled. Empty when every package of the deployment is registered by its own code. |
326+
| **packages** | `({ id: string; name: string; version: string; label?: string; … } & Record<string, any>)[]` | ✅ | Every package whose registry row's `source` is not `code` (the marketplace and authored packages, a `.custom` pair among them), enabled or not, each once and each as `GET /packages/:id/export` answers it, dependencies first: an entry comes after every entry its package's `manifest.dependencies` names, so a `.custom` pair comes after its base. Install the entries in this order; never re-sort them. No entry carries an enabled state, so every entry installs enabled. Empty when every package of the deployment is registered by its own code. |
320327

321328
### Nested Shape: `PackageExportBundle.packages[number]`
322329

@@ -326,6 +333,7 @@ A field or flow another package contributes to one of the base's objects.
326333
| **name** | `string` | ✅ | The exported package's machine name. |
327334
| **version** | `string` | ✅ | The exported package's version. |
328335
| **label** | `string` | optional | Display label, when the package declares one. |
336+
| **withheldMaps** | `{ type: 'connector'; name: string; path: Enum<'providerConfig.headers' \| 'providerConfig.defaultHeaders' \| …> }[]` | optional | [PLANNED — not yet served] Declared ahead of its producer (ADR-0049): no export writes this list yet and no install door reads it yet. Every map the export withheld WHOLE because a served read never carries it: a connector's header and environment-variable maps. The item is written with that map absent, so once served an import reads this list to tell an incomplete map from one the package never had, and names each one for an operator to re-enter. Positions only: no header or variable name is listed. Absence says nothing about what was withheld when the exporter predates this key; from an exporter that writes it, absence means nothing was withheld. |
329337

330338

331339
---
@@ -340,6 +348,15 @@ A field or flow another package contributes to one of the base's objects.
340348
| **name** | `string` | ✅ | The exported package's machine name. |
341349
| **version** | `string` | ✅ | The exported package's version. |
342350
| **label** | `string` | optional | Display label, when the package declares one. |
351+
| **withheldMaps** | `{ type: 'connector'; name: string; path: Enum<'providerConfig.headers' \| 'providerConfig.defaultHeaders' \| …> }[]` | optional | [PLANNED — not yet served] Declared ahead of its producer (ADR-0049): no export writes this list yet and no install door reads it yet. Every map the export withheld WHOLE because a served read never carries it: a connector's header and environment-variable maps. The item is written with that map absent, so once served an import reads this list to tell an incomplete map from one the package never had, and names each one for an operator to re-enter. Positions only: no header or variable name is listed. Absence says nothing about what was withheld when the exporter predates this key; from an exporter that writes it, absence means nothing was withheld. |
352+
353+
### Nested Shape: `PackageExportManifest.withheldMaps[number]`
354+
355+
| Property | Type | Required | Description |
356+
| :--- | :--- | :--- | :--- |
357+
| **type** | `'connector'` | ✅ | Metadata type of the item whose map was withheld. |
358+
| **name** | `string` | ✅ | Name of the item whose map was withheld. |
359+
| **path** | `Enum<'providerConfig.headers' \| 'providerConfig.defaultHeaders' \| …>` | ✅ | Dot path of the withheld map inside the item. |
343360

344361

345362
---

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.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-
| `api/` | 489 |
22+
| `api/` | 490 |

‎packages/spec/authorable-surface/api.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,7 @@
13061306
"api/PackageExportManifest:label",
13071307
"api/PackageExportManifest:name",
13081308
"api/PackageExportManifest:version",
1309+
"api/PackageExportManifest:withheldMaps",
13091310
"api/PackageInstallManifest:capabilities [RETIRED]",
13101311
"api/PackageInstallManifest:configuration [RETIRED]",
13111312
"api/PackageInstallManifest:contributes",

‎packages/spec/src/api/package-lifecycle.test.ts‎

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
GetPackageCustomizationsResponseSchema,
2626
} from './package-lifecycle.zod';
2727
import { PackagePublishResultSchema as SystemPackagePublishResultSchema } from '../system/metadata-persistence.zod';
28+
import { CONNECTOR_CREDENTIAL_MAP_PATHS, redactConnectorItem } from '../integration/connector-credential-redaction';
2829
import { InstalledPackageSourceSchema } from '../kernel/package-registry.zod';
2930

3031
describe('the ruled re-export of PackagePublishResultSchema into the `/api` namespace', () => {
@@ -197,7 +198,7 @@ describe('RollbackToPackageCommitResponseSchema declares the COMMIT-rollback bod
197198
});
198199
});
199200

200-
describe('PackageExportManifestSchema declares the four fixed keys and stays open', () => {
201+
describe('PackageExportManifestSchema declares the five fixed keys and stays open', () => {
201202
/** A verbatim-shaped capture of a real `assemblePackageManifest` return. */
202203
const realResponse = {
203204
id: 'com.example.crm',
@@ -214,7 +215,7 @@ describe('PackageExportManifestSchema declares the four fixed keys and stays ope
214215
if (result.success) expect(result.data).toEqual(realResponse);
215216
});
216217

217-
it('requires exactly the four fixed keys — a manifest with no items still parses', () => {
218+
it('requires only the three identity keys — a manifest with no items still parses', () => {
218219
const bare = { id: 'com.example.empty', name: 'empty', version: '1.0.0' };
219220
expect(PackageExportManifestSchema.safeParse(bare).success).toBe(true);
220221
});
@@ -224,6 +225,57 @@ describe('PackageExportManifestSchema declares the four fixed keys and stays ope
224225
});
225226
});
226227

228+
describe('PackageExportManifestSchema.withheldMaps names each map the export withheld whole (#28640 ruling ③A)', () => {
229+
const identity = { id: 'com.example.crm', name: 'example-crm', version: '2.1.0' };
230+
/** The connector item as the export writes it: served, so its header map is already absent. */
231+
const servedConnector = { name: 'stripe', label: 'Stripe', type: 'api', provider: 'rest', providerConfig: { baseUrl: 'https://api.stripe.example' } };
232+
233+
it('parses an export that lists a withheld map and PRESERVES it — the item body stays as served', () => {
234+
const manifest = {
235+
...identity,
236+
connectors: [servedConnector],
237+
withheldMaps: [{ type: 'connector', name: 'stripe', path: 'providerConfig.headers' }],
238+
};
239+
const result = PackageExportManifestSchema.safeParse(manifest);
240+
expect(result.success).toBe(true);
241+
if (result.success) expect(result.data).toEqual(manifest);
242+
});
243+
244+
it('is declared ahead of its producer: the describe opens with the planned marker and names the ADR, never a tracker id', () => {
245+
const description = PackageExportManifestSchema.shape.withheldMaps.description ?? '';
246+
expect(description).toMatch(/^\[PLANNED — not yet served\] /);
247+
expect(description).toContain('ADR-0049');
248+
expect(description).not.toMatch(/#\d{3,}/);
249+
});
250+
251+
it('is optional — an export that withheld nothing, or one from before the key, still parses', () => {
252+
expect(PackageExportManifestSchema.safeParse(identity).success).toBe(true);
253+
});
254+
255+
it('spells each position exactly as the connector redactor reports it — the same four maps, nothing else', () => {
256+
const declared = [...PackageExportManifestSchema.shape.withheldMaps.unwrap().element.shape.path.options];
257+
// The redactor's own list of the maps it withholds whole …
258+
expect(declared).toEqual(CONNECTOR_CREDENTIAL_MAP_PATHS.map((p) => p.join('.')));
259+
// … and the spelling its `redactedKeys` names them by, read off a real redaction.
260+
const map = { Authorization: 'x' };
261+
const { redactedKeys } = redactConnectorItem({
262+
...servedConnector,
263+
providerConfig: { headers: map, defaultHeaders: map, transport: { headers: map, env: { TOKEN: 'x' } } },
264+
});
265+
expect([...redactedKeys].sort()).toEqual([...declared].sort());
266+
});
267+
268+
it.each([
269+
['a position outside the four', { type: 'connector', name: 'stripe', path: 'auth.token' }],
270+
['a position spelled without its providerConfig root', { type: 'connector', name: 'stripe', path: 'defaultHeaders' }],
271+
['a type no export withholds a map of yet', { type: 'webhook', name: 'hook', path: 'providerConfig.headers' }],
272+
['a list of the withheld header names — maps are withheld whole', { type: 'connector', name: 'stripe', path: 'providerConfig.headers', keys: ['Authorization'] }],
273+
['an entry with no item name', { type: 'connector', path: 'providerConfig.headers' }],
274+
])('refuses %s', (_label, entry) => {
275+
expect(PackageExportManifestSchema.safeParse({ ...identity, withheldMaps: [entry] }).success).toBe(false);
276+
});
277+
});
278+
227279
describe('PackageExportBundleSchema declares the whole-deployment export as composed per-package exports (#28914)', () => {
228280
/**
229281
* The bundle the card's done criteria name: a base, its `.custom` pair and a

‎packages/spec/src/api/package-lifecycle.zod.ts‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@
3737
* declared a VERSION rollback against the live COMMIT-rollback path;
3838
* `RollbackToPackageCommitResponseSchema` below is the true contract,
3939
* authored after that retirement per the ruling's sequencing (3A).
40+
*
41+
* Two members are not transcriptions but DECLARED AHEAD OF THEIR PRODUCER
42+
* (ADR-0049), each describe opening `[PLANNED — not yet served]` until the
43+
* change that serves it drops the marker: the `nav` arm of
44+
* `GetPackageCustomizationsResponseSchema.contributions` (#27928) and
45+
* `PackageExportManifestSchema.withheldMaps` (#28640 ruling ③A, served by
46+
* #29342). Their own docblocks say what is not served yet.
4047
*/
4148

4249
import { z } from 'zod';
@@ -175,16 +182,56 @@ export const RollbackToPackageCommitResponseSchema = lazySchema(() => z.object({
175182
* (`runtime/src/domains/packages.ts`) builds the key set DYNAMICALLY from the
176183
* metadata type registry — one plural key per type present
177184
* (`manifest[plural] = items.map(clean)`), partitioning `views` per #5320 —
178-
* plus the four fixed keys below. Only the fixed keys are pinnable;
185+
* plus the five fixed keys below. Only the fixed keys are pinnable;
179186
* enumerating the registry here would freeze this contract against future
180187
* metadata types, so everything else deliberately falls to the open
181188
* catch-all. Freezes nothing.
189+
*
190+
* `withheldMaps` (#28640 ruling ③A) names each map position the export
191+
* withheld whole. The export reads SERVED items, so the connector redactor
192+
* (`CONNECTOR_CREDENTIAL_MAP_PATHS`, `integration/connector-credential-redaction.ts`)
193+
* has already dropped each of its four header / env maps, and the item is
194+
* written with the map simply absent; this list is how an import tells an
195+
* incomplete map from one that was never written. Positions only — never a
196+
* header or variable name — so the credential heuristic stays the
197+
* redactor's, not a contract of the export. The path vocabulary is the
198+
* redactor's own `redactedKeys` spelling of those four positions, and
199+
* `package-lifecycle.test.ts` holds the two equal.
200+
*
201+
* DECLARED AHEAD OF ITS PRODUCER (ADR-0049): `withheldMaps` is declared
202+
* before any export writes it and before any install door reads it. Until the
203+
* server half (#29342) makes `assemblePackageManifest` write the list and both
204+
* install doors name each incomplete map, no export carries it; its describe
205+
* opens `[PLANNED — not yet served]`, and #29342 drops the marker in the change
206+
* that serves it (the pin is in `package-lifecycle.test.ts`). Its `door` row in
207+
* `UNMANAGED_INSTALL_KEY_DISPOSITIONS` is live today: it keeps the unmanaged
208+
* import from refusing an export that carries the key.
182209
*/
183210
export const PackageExportManifestSchema = lazySchema(() => z.object({
184211
id: z.string().describe('The exported package\'s id.'),
185212
name: z.string().describe('The exported package\'s machine name.'),
186213
version: z.string().describe('The exported package\'s version.'),
187214
label: z.string().optional().describe('Display label, when the package declares one.'),
215+
// Declared ahead of its producer: #29342 writes and reads it, and drops the
216+
// `[PLANNED — not yet served]` marker in that change.
217+
withheldMaps: z.array(z.object({
218+
type: z.literal('connector').describe('Metadata type of the item whose map was withheld.'),
219+
name: z.string().describe('Name of the item whose map was withheld.'),
220+
path: z.enum([
221+
'providerConfig.headers',
222+
'providerConfig.defaultHeaders',
223+
'providerConfig.transport.headers',
224+
'providerConfig.transport.env',
225+
]).describe('Dot path of the withheld map inside the item.'),
226+
}).strict()).optional().describe(
227+
'[PLANNED — not yet served] Declared ahead of its producer (ADR-0049): no export writes this list '
228+
+ 'yet and no install door reads it yet. Every map the export withheld WHOLE because a served read '
229+
+ 'never carries it: a connector\'s header and environment-variable maps. The item is written with '
230+
+ 'that map absent, so once served an import reads this list to tell an incomplete map from one the '
231+
+ 'package never had, and names each one for an operator to re-enter. Positions only: no header or '
232+
+ 'variable name is listed. Absence says nothing about what was withheld when the exporter predates '
233+
+ 'this key; from an exporter that writes it, absence means nothing was withheld.',
234+
),
188235
}).catchall(z.unknown().describe(
189236
'One key per metadata type present in the package (plural spelling, e.g. '
190237
+ '`objects`, `views`), each an array of cleaned item bodies. The key set '

0 commit comments

Comments
 (0)