Skip to content

Commit e756782

Browse files
feat(runtime): a package export names each connector map it withheld; install-local names each as incomplete (#29746)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8CBufL2gFph9TKgWijoL1TU)_ Fixes #29342 Clause-②: yes (widening) Metadata-protocol: none One narrowing arm, named here in prose as the seat directed (amendment 6592752231): `POST /marketplace/install-local` now refuses a `withheldMaps` value that the export's own declaration refuses, answering `PLUGIN_MANIFEST_INVALID` before any write in either mode. Before this change the door ignored the key whatever its value. No producer wrote the key before this PR, so only a hand-written list can be refused. ## 维护者速读 - 改了什么:包导出(`GET /packages/:id/export`)现在写出 `withheldMaps`(整库导出 `GET /packages/export` 的每个条目同样带上,因其逐包复用同一导出),逐条列出被整张扣下的连接器 header/env 表;install-local 的托管与非托管两种安装都在应答里逐条点名这些不完整的表,并打一条 warn。 - 为什么改:导出读的是脱敏后的条目,这四张表被整张拿掉后,导入方分不清"被扣下"和"本来就没有"(#28640 裁决 ③A,席位裁决 A 6578196369)。 - 风险与回滚:导出对每个连接器多一次按名执行读;拿不到执行读就失败而不是省略键。安装门对格式错误的 `withheldMaps` 新增 400 拒绝。回滚即还原本 PR。 - 先看:`packages/runtime/src/domains/packages.ts` 的 `withheldConnectorMaps`;`packages/cloud-connection/src/marketplace-install-local-plugin.ts` 的 `judgeWithheldMaps` 及两处应答;`packages/spec/src/api/package-lifecycle.zod.ts` 的描述改动。 - 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-export-withheld-maps.test.ts`,预期 6 条全绿,第一条列出四个位置。 - 需要你做的:无。契约复核 PASS(6594653702),两轮增量复核 PASS(6595333171、6596808111),席位落地。 ## What changes - **The export writes `withheldMaps`.** `assemblePackageManifest` (runtime `packages.ts`) now lists `{ type: 'connector', name, path }` for every connector map the served read withheld whole. These are the four positions `PackageExportManifestSchema.withheldMaps` declares: `providerConfig.headers`, `providerConfig.defaultHeaders`, `providerConfig.transport.headers` and `providerConfig.transport.env`. The new hunk sits at the end of the function, after the `label`, `namespace` and `dependencies` lines (the last two from #29617), apart from the per-package `declaresOverrideOf` filter that #29687 (card #29649) edits. - **How the verdict is read.** The export writes SERVED items, where a withheld map is simply absent. So each exported connector's stored body is read on the protocol's by-name execution face, `getMetaItemForExecution({ type: 'connector', name, packageId })`, and handed to the same `connector` redactor the served read ran (`getMetadataTypeRedactor('connector')`). Every position its `redactedKeys` names that the declaration lists is written. That face's docblock allows a door to read a decision off it, as long as no byte of the body reaches the response. Only the position leaves: no header or variable name, no value. - **Why the by-name read is the item the per-package list served.** The two reads differ only in the effective layer's declared-override step (ADR-0146 D8). `connector` is not an overridable type: it has no row in `OVERRIDE_TYPE_CLASSIFICATION`, and the write gate refuses an override declaration on a type outside it. So under the package's own id, the by-name read answers the package's own connector, the one the per-package list served. - **No guessed verdict.** If the protocol has no execution face, or the face answers no body for an exported connector, the export fails (500) instead of omitting the key. The declaration says that from an exporter that writes the key, an absent key means nothing was withheld. - **Both install modes read it.** `POST /marketplace/install-local` (cloud-connection, the door the Studio import button posts to) parses the body's `withheldMaps` through `PackageExportManifestSchema.shape.withheldMaps` before any write. The managed and unmanaged answers each gain `withheldMaps`, the list as the export wrote it, and one `warn` line names every map. The key is left out when the body carries no list, so that answer's key set is unchanged. The package is accepted and never refused because a map is incomplete (ruling A). - **Spec.** The `[PLANNED — not yet served]` marker is dropped from `withheldMaps`' describe, and its pin in `package-lifecycle.test.ts` flips to assert that the marker is absent. The module and schema docblocks now say the key is served. The comment on the `door` row in `kernel/unmanaged-install.ts` says the same (comment only, approved in amendment 6592752231). `content/docs/references/api/package-lifecycle.mdx` is regenerated by `check:generated --fix`. After merging #29742 (nav served) and #29755 (#28914, the whole-deployment bundle served), no member of `package-lifecycle.zod.ts` is declared ahead any more, so the header's declared-ahead paragraph is removed (85d56fc) and the mdx regenerated. - **Changeset:** `29342-export-withheld-connector-maps.md`, patch for runtime, cloud-connection and spec, carrying both declaration lines and the narrowing arm in prose. ## Tests - `packages/runtime/src/domains/packages-export-withheld-maps.test.ts` (6 tests). It uses the real dispatcher, with a protocol double whose served list runs the real `connector` redactor. It checks: all four positions in order; the exported items as served, with no header name or value in the manifest; a connector with no map, or with a cleared map, is not listed; a package with no connectors asks the face nothing; a protocol without the face, and a missing body, each fail the export. - `packages/cloud-connection/src/marketplace-install-local-withheld-maps.test.ts` (7 tests), at the plugin's real route. Managed and unmanaged answers each name the list, with one warn line. A body without the list gets no key and no warn. Three malformed lists are refused `PLUGIN_MANIFEST_INVALID`, 400, in both modes, with nothing registered, ledgered or imported. - `packages/qa/dogfood/test/export-withheld-connector-maps.dogfood.test.ts` (4 tests) runs on the real showcase composition. It installs a package with three connectors, two carrying header maps, then exports it. The export lists the three maps and carries no header name or value. A managed re-install of the export names them. On a second boot that never held the package, an unmanaged install of the export also names them. <details><summary>Red first, by reverse verification on the base source (744211f)</summary> `git restore --source=744211fc` of the two runtime and cloud-connection source files, with on-disk marker counts `0 0` and runtime and cloud-connection rebuilt. Then `ablation-dist-preflight` confirmed that `withheldConnectorMaps` is absent from `packages/runtime/dist`. - runtime unit: 3 failed, 3 passed. The "lists every withheld position" test and both no-guess tests were red. - cloud-connection unit: 5 failed, 2 passed. Both naming tests and the 3 refusals were red. - dogfood: 3 failed, 1 passed. The export listing, the managed answer and the unmanaged answer were red. Restored with `git restore --source=HEAD --staged --worktree`, `git status --porcelain` empty, rebuilt, preflight found the marker present again, and the dogfood ran 4/4 green. </details> <details><summary>Ablation of the narrowing arm (seat ask)</summary> `node scripts/ablation-replace.mjs` turned `if (!declared.success)` into `if (!declared.success && false)` in `judgeWithheldMaps`: anchor 1 → 0, blob f2cf413 → 84f0fc0a. Result: 3 failed | 4 passed, and the failures were exactly the three `PLUGIN_MANIFEST_INVALID` cases. Restored: the blob equals HEAD, and `git diff HEAD` is empty. </details> ## Acceptance notes - **The export carries no `type` or `installModes`, so it does not import unmanaged as written.** Measured on the showcase: `importUnmanagedPackage` refused the bare export with `manifest.type: Invalid option`. The per-package export writes only `id`, `name`, `version`, `label`, `namespace`, `dependencies` and its collections. This belongs to the export's manifest-key family; carrier: #28914. The dogfood adds the two keys an operator adds before importing an export as a copy, and its header says so. - **The unmanaged install answers "the reason is in the server log" for a 4xx that it never logs.** Measured in the same run: a 400 `VALIDATION_ERROR` answered `The unmanaged install of 'com.dogfood.withheldmaps' failed; the reason is in the server log.`, and the server log had no line with the reason. `installUnmanaged` logs only when `resolved.status >= 500`. Reported to the seat for filing (class a). Not fixed here. Filed by the seat as #29822. - **Webhook `headers`** (`credential: 'map'`) are dropped in the same silent way. Ruling A keeps them out of scope. - **Serial note:** #29687 (card #29649) also edits `assemblePackageManifest`. It landed at 33ee9aa and is merged into this branch (009c0b7). After the merge, this PR's hunk is still separate from its `declaresOverrideOf` filter. #29742 (seat 3) and #29755 (#28914, seat 4) landed after it and are merged in (c5f0877 at b3d1415). `assemblePackageExportBundle` composes the per-package export, so each bundle entry carries `withheldMaps` too; `PackageExportBundleSchema.packages` is `PackageExportManifestSchema[]`, which already declares the key. ## Verification Unit suites, typecheck, gates and lint below were measured at `583fce44`, before the merge, and re-run at head `c5a34147`, after merging `origin/main` (33ee9aa). Post-merge figures are given where they differ. - `pnpm --filter @objectstack/runtime test`: 530 files, 8518 passed, 19 skipped; after the merge, 532 files, 8529 passed, 19 skipped. `pnpm --filter @objectstack/cloud-connection test`: 68 files, 820 passed, the same after the merge. Spec `package-lifecycle.test.ts` and `unmanaged-install.test.ts`: 63 passed; after the merge, 67 passed. Dogfood file: 4/4 after the merge. `pnpm --filter @objectstack/runtime --filter @objectstack/cloud-connection typecheck`: exit 0. - `pnpm --filter @objectstack/spec check:generated`: `check:docs` was stale and was fixed with `--fix` (one artifact). The other 13 were up to date. - `node scripts/pm/dispatch-gates.mjs --commands`: 105 families. All 105 exit 0, three of them after their prerequisites were met: the clone was unshallowed for `check-plugin-teardown-shape --self-test`, and the client and plugin packages were built for `check:skill-examples` and `check:dual-build-cjs-loads`. `--ran` reconciliation: 105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN. - `pnpm lint`: exit 0. - Full dogfood battery: not run. The title has no `!` and Clause-② has no `(narrowing)` arm. Re-run at head `85d56fc4` (after merging `origin/main` b3d1415): typecheck for spec, runtime and cloud-connection exit 0; spec package-lifecycle and unmanaged-install 67 passed; runtime 535 files, 8560 passed, 19 skipped; cloud-connection 68 files, 820 passed; dogfood file 4/4; `dispatch-gates --commands` 105 derived, 105 exit 0, `--ran` 105/105, 0 NOT-MEASURED. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbYXRMR5S5B7JSmKkV7 --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbYXRMR5S5B7JSmKkV7)_ <!-- signed: domain:server#5 · os-warren · session_01JQwxbZUR3xvHyDPCjNL5P4 --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7a25b39 commit e756782

10 files changed

Lines changed: 691 additions & 41 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/runtime': patch
3+
'@objectstack/cloud-connection': patch
4+
'@objectstack/spec': patch
5+
---
6+
7+
feat(runtime): a package export names each connector map it withheld, and both install modes of install-local name each one as incomplete
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: none
11+
12+
`GET /api/v1/packages/:id/export` writes served items, and the served read withholds four connector maps whole: `providerConfig.headers`, `providerConfig.defaultHeaders`, `providerConfig.transport.headers` and `providerConfig.transport.env`. An exported connector used to arrive with such a map simply absent, so an import could not tell a withheld map from one the package never had. Now:
13+
14+
- **The export lists each one.** The manifest's `withheldMaps` (declared on `PackageExportManifestSchema`) holds `{ type: 'connector', name, path }` for every map withheld. Each exported connector's stored body is judged by the same `connector` redactor the served read ran, read through the protocol's by-name execution face. Only the position leaves that read: no header or variable name, and no value. A package that withheld nothing carries no key. An export that cannot judge (a protocol without `getMetaItemForExecution`, or a connector that read answers no body for) fails rather than omitting the key, because from an exporter that writes it, an absent key means nothing was withheld.
15+
- **Both install modes name each one.** `POST /api/v1/marketplace/install-local`, managed and unmanaged, accepts the package and answers `withheldMaps`, the list as the export wrote it, plus one `warn` line naming every map. The key is left out when the body carries no list, so that answer's key set is unchanged.
16+
- **One narrowing, at the door.** A `withheldMaps` value the export's declaration refuses is now answered `PLUGIN_MANIFEST_INVALID` (400 for an inline manifest, 502 for a cloud snapshot) before any write, in either mode, the way an invalid `installModes` already is. Before this change the door ignored the key, whatever its value. No export wrote the key before this release, so only a hand-written list can be refused. Fix: send the list exactly as the export wrote it, or drop it.
17+
- **Spec.** The `[PLANNED — not yet served]` marker is dropped from `withheldMaps`' describe, and the docblocks now say the key is served.

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

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,6 @@ 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 declarations are not transcriptions but DECLARED AHEAD OF THEIR
47-
PRODUCER (ADR-0049), each describe opening `[PLANNED — not yet served]`
48-
until the change that serves it drops the marker:
49-
`PackageExportManifestSchema.withheldMaps` (#28640 ruling ③A, served by
50-
#29342) and `PackageExportBundleSchema` (`GET /packages/export`, #28914).
51-
Their own docblocks say what is not served yet.
52-
5346
<Callout type="info">
5447
**Source:** `packages/spec/src/api/package-lifecycle.zod.ts`
5548
</Callout>
@@ -335,7 +328,7 @@ Whole-deployment export, the answer of `GET /packages/export` (`os package expor
335328
| **namespace** | `string` | optional | The namespace the package was installed with, as its registry row's manifest declares it; absent when the row declares none. An install of the export keeps it instead of deriving one from the id, so a `.custom` pair is reinstalled under its own namespace rather than `custom`. |
336329
| **dependencies** | `Record<string, string>` | optional | The packages this package depends on, each id mapped to its version range, as its registry row's manifest declares them; absent when the row declares none. An install of the export orders it after each of them (ADR-0141 D5), and a `.custom` pair keeps the edge to its base that admits its overrides (ADR-0146 D2). |
337330
| **label** | `string` | optional | Display label, when the package declares one. |
338-
| **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. |
331+
| **withheldMaps** | `{ type: 'connector'; name: string; path: Enum<'providerConfig.headers' \| 'providerConfig.defaultHeaders' \| …> }[]` | optional | 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 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. |
339332

340333

341334
---
@@ -352,7 +345,7 @@ Whole-deployment export, the answer of `GET /packages/export` (`os package expor
352345
| **namespace** | `string` | optional | The namespace the package was installed with, as its registry row's manifest declares it; absent when the row declares none. An install of the export keeps it instead of deriving one from the id, so a `.custom` pair is reinstalled under its own namespace rather than `custom`. |
353346
| **dependencies** | `Record<string, string>` | optional | The packages this package depends on, each id mapped to its version range, as its registry row's manifest declares them; absent when the row declares none. An install of the export orders it after each of them (ADR-0141 D5), and a `.custom` pair keeps the edge to its base that admits its overrides (ADR-0146 D2). |
354347
| **label** | `string` | optional | Display label, when the package declares one. |
355-
| **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. |
348+
| **withheldMaps** | `{ type: 'connector'; name: string; path: Enum<'providerConfig.headers' \| 'providerConfig.defaultHeaders' \| …> }[]` | optional | 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 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. |
356349

357350
### Nested Shape: `PackageExportManifest.withheldMaps[number]`
358351

‎packages/cloud-connection/src/marketplace-install-local-plugin.ts‎

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@
7575
* none of which writes, and before any write.
7676
* A mode the package's `installModes` does not permit answers
7777
* `422 INSTALL_MODE_NOT_PERMITTED`, before any write.
78+
* Either mode reads the body's `withheldMaps` — the connector header
79+
* and environment maps a package export withheld whole (#28640
80+
* ruling ③A) — accepts the package, and names each map in its
81+
* answer's `withheldMaps` and in one `warn` line; a list the export's
82+
* declaration refuses answers `PLUGIN_MANIFEST_INVALID` (400 inline,
83+
* 502 cloud), before any write.
7884
*
7985
* [ADR-0126 §6 wall 3] A managed RE-install over an installed package
8086
* asks the security plugin's access upgrade check
@@ -187,6 +193,9 @@ import {
187193
unmanagedInstallValueCarries,
188194
type InstallMode,
189195
} from '@objectstack/spec/kernel';
196+
// [#28640 ruling ③A] The export's list of the connector maps it withheld whole,
197+
// read by both install modes through its one declaration.
198+
import { PackageExportManifestSchema, type PackageExportManifest } from '@objectstack/spec/api';
190199
// [#21762] ADR-0087 D1's protocol handshake, its brand predicate and the ONE
191200
// answer both package-install doors give its refusal. Imported from the
192201
// producer, never restated: `POST /api/v1/packages` answers through the same
@@ -602,6 +611,57 @@ function isPlainObject(value: unknown): value is Record<string, any> {
602611
return value !== null && typeof value === 'object' && !Array.isArray(value);
603612
}
604613

614+
/** One entry of an export's `withheldMaps` ({@link PackageExportManifestSchema}). */
615+
type WithheldMap = NonNullable<PackageExportManifest['withheldMaps']>[number];
616+
617+
/**
618+
* [#28640 ruling ③A] The connector maps the export that produced this body
619+
* withheld whole — its `withheldMaps`, parsed through the export's own
620+
* declaration ({@link PackageExportManifestSchema}), by reference. Read by
621+
* BOTH install modes before any write, so each answer names every map that
622+
* arrives incomplete instead of letting it read as one the package never had.
623+
*
624+
* An absent list is `[]`: a body that is not an export, or an export that
625+
* withheld nothing. A list the declaration refuses is refused here, the way
626+
* an invalid `installModes` is ({@link judgeInstallMode}): it can only have
627+
* been written by hand, and guessing which maps it meant is the silent answer
628+
* the list exists to replace. The import never refuses a package FOR an
629+
* incomplete map (the ruling: accept, and name each one).
630+
*/
631+
function judgeWithheldMaps(
632+
manifest: Record<string, any>,
633+
manifestId: string,
634+
invalidManifestStatus: 400 | 502,
635+
): { ok: true; withheldMaps: WithheldMap[] } | { ok: false; status: number; error: { code: string; message: string } } {
636+
const declared = PackageExportManifestSchema.shape.withheldMaps.safeParse(manifest.withheldMaps);
637+
if (!declared.success) {
638+
const [issue] = declared.error.issues;
639+
return {
640+
ok: false,
641+
status: invalidManifestStatus,
642+
error: {
643+
code: 'PLUGIN_MANIFEST_INVALID',
644+
message: `Package '${manifestId}' carries an invalid \`withheldMaps\`: ${issue?.message ?? 'refused by its declaration'}`
645+
+ `${issue && issue.path.length > 0 ? ` (at \`withheldMaps.${issue.path.join('.')}\`)` : ''}. `
646+
+ 'Send the list as the package export wrote it, or omit it.',
647+
},
648+
};
649+
}
650+
return { ok: true, withheldMaps: declared.data ?? [] };
651+
}
652+
653+
/**
654+
* [#28640 ruling ③A] The operator's line for an install that received
655+
* withheld maps: each one named, once, at `warn` — the connector is installed
656+
* and runs, without the headers or environment variables the export held back.
657+
*/
658+
function withheldMapsLogLine(manifestId: string, mode: InstallMode, withheldMaps: readonly WithheldMap[]): string {
659+
const named = withheldMaps.map((m) => `${m.type} '${m.name}' ${m.path}`).join(', ');
660+
return `[MarketplaceInstallLocal] installed ${manifestId} ${mode} with ${withheldMaps.length} connector map(s) the `
661+
+ `package export withheld whole, each now absent: ${named}. The connector sends none of those headers or `
662+
+ 'environment variables until each map is supplied again where the connector is authored.';
663+
}
664+
605665
/**
606666
* [ADR-0131 D6] The package manifest an install body DECLARES — where each of
607667
* the two single-package shapes this door accepts carries it: a compiled
@@ -1925,6 +1985,15 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
19251985
}, UNRUNNABLE_REFUSAL_STATUS);
19261986
}
19271987

1988+
// 1d'. [#28640 ruling ③A] THE MAPS THE EXPORT WITHHELD, read for both
1989+
// modes before any write: each answer names every connector map
1990+
// that arrives incomplete ({@link judgeWithheldMaps}). Accepted,
1991+
// never refused for, as the ruling has it; only a list the export's
1992+
// declaration refuses is.
1993+
const withheld = judgeWithheldMaps(manifest, manifestId, inlineManifest ? 400 : 502);
1994+
if (!withheld.ok) return c.json({ success: false, error: withheld.error }, withheld.status as any);
1995+
const { withheldMaps } = withheld;
1996+
19281997
// ── [ADR-0131 D6] The `unmanaged` install: a copy, not a registration.
19291998
// The package's metadata is copied ONCE into a new writable base in
19301999
// the environment ledger by the import verb, and the steps below —
@@ -1940,6 +2009,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
19402009
version,
19412010
versionId: resolvedVersionId,
19422011
userId,
2012+
withheldMaps,
19432013
});
19442014
}
19452015

@@ -2308,6 +2378,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
23082378
// The door is named only when it can store one of them: a listing that
23092379
// says none is writable (a walled posture) gets the other places.
23102380
const door = described?.writable === undefined || unsetRefs.some((r) => described.writable!.has(r));
2381+
if (withheldMaps.length > 0) ctx.logger?.warn?.(withheldMapsLogLine(manifestId, 'managed', withheldMaps));
23112382

23122383
return c.json({
23132384
success: true,
@@ -2333,6 +2404,10 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
23332404
// that declares none, so that answer's key set is unchanged.
23342405
...(datasources !== undefined ? { datasources } : {}),
23352406
...(unsetRefs.length > 0 ? { credentialNote: installedCredentialNote(unsetRefs, { door }) } : {}),
2407+
// [#28640 ruling ③A] Each connector map the package export
2408+
// withheld whole, so it arrived absent. Omitted when there are
2409+
// none, so that answer's key set is unchanged.
2410+
...(withheldMaps.length > 0 ? { withheldMaps } : {}),
23362411
upgradedFrom: conflict === 'marketplace' ? 'previous-marketplace-version' : null,
23372412
// [ADR-0146 D2, #28410] Names installed dependents already held,
23382413
// undeclared, that this version now ships (step 2d). Omitted when
@@ -3538,14 +3613,21 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
35383613
* either spelling, and the copy does not keep, with the package's
35393614
* reason — is the verb's own
35403615
* list, relayed as the verb states it, so the installer reads why the
3541-
* vendor locked an item before editing it.
3616+
* vendor locked an item before editing it;
3617+
* - `withheldMaps` — every connector map the package export withheld
3618+
* whole, read by {@link handleInstall} before the verb runs — is on the
3619+
* answer and on a `warn` line: the copy carries each map absent.
35423620
*/
35433621
private installUnmanaged = async (
35443622
c: any,
35453623
ctx: PluginContext,
3546-
args: { manifest: Record<string, any>; manifestId: string; version: string; versionId: string; userId: string | null },
3624+
args: {
3625+
manifest: Record<string, any>; manifestId: string; version: string; versionId: string; userId: string | null;
3626+
/** [#28640 ruling ③A] The connector maps the export withheld ({@link judgeWithheldMaps}). */
3627+
withheldMaps: readonly WithheldMap[];
3628+
},
35473629
): Promise<Response> => {
3548-
const { manifest, manifestId, version, versionId, userId } = args;
3630+
const { manifest, manifestId, version, versionId, userId, withheldMaps } = args;
35493631
let protocol: UnmanagedImporter | undefined;
35503632
try { protocol = ctx.getService<UnmanagedImporter>('protocol'); } catch { /* no protocol service */ }
35513633
if (!protocol || typeof protocol.importUnmanagedPackage !== 'function') {
@@ -3598,6 +3680,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
35983680
+ `${installedFrom.packageId}${installedFrom.version ? `@${installedFrom.version}` : ''}`
35993681
+ `${userId ? ` by ${userId}` : ''}. Nothing is registered under '${manifestId}'; the copy follows no upgrade.`,
36003682
);
3683+
if (withheldMaps.length > 0) ctx.logger?.warn?.(withheldMapsLogLine(manifestId, 'unmanaged', withheldMaps));
36013684
const declaresSeeds = unmanagedInstallValueCarries(packageBody.data);
36023685
return c.json({
36033686
success: true,
@@ -3613,6 +3696,10 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
36133696
copiedCount: result.copiedCount,
36143697
copied: result.copied,
36153698
strippedLocks: result.strippedLocks,
3699+
// [#28640 ruling ③A] Each connector map the package export
3700+
// withheld whole, so its copy carries it absent. Omitted when
3701+
// there are none, so that answer's key set is unchanged.
3702+
...(withheldMaps.length > 0 ? { withheldMaps: [...withheldMaps] } : {}),
36163703
...(declaresSeeds
36173704
? {
36183705
seeded: {

0 commit comments

Comments
 (0)