Skip to content

Commit d2c4874

Browse files
fix(service-automation,objectql,cloud-connection)!: an installed package's connector entry never reads the host's files (#28587)
epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U Fixes #28534 Clause-②: no (narrowing) Metadata-protocol: narrowed Skills-docs: none — no `skills/**` file teaches `_packageId` or an install door's `source`; the refusal concerns a key only the registration writes ## 维护者速读 - **改了什么**:从市场安装(install-local)的包,其连接器条目不再能按宿主的根目录读文件或启动进程:相对路径的文件引用一律被拒,报错里写明条目、包、引用和改法。另外,防止被安装的包冒充宿主:(1) objectql 拒绝自带 `_packageId`、且与注册它的包不一致的条目;(2) install-local 把每个子包都登记成 `marketplace`,首次安装和重启后重新加载都一样;(3) 查不到记录的包 id 一律按"已安装"处理并拒绝,不再默认算作宿主的。 - **为什么改**:实测在 `a52191f6` 上,装进 showcase 的包能读到宿主自己的 OpenAPI 文档,也能在宿主目录下运行 MCP 脚本。第一版 PR 只看注册表里的来源记录,评审 6554878186 指出包能自己写 `_packageId` 或 `source: 'code'` 绕过,所以补了 (1) (2) (3)。 - **风险与回滚**:这是一次收窄(`Metadata-protocol: narrowed`),objectql 和 cloud-connection 升 minor。受影响的只有两类:手写了别的包 `_packageId` 的条目(仓库里没有任何正常产出会这样写),和已安装包里的 stdio MCP 条目(以后要改用 http)。回滚就是 revert 这一个 PR。 - **先看**:`packages/objectql/src/engine.ts` 里的 `collectionItemOwnerStampRefusal` 和登记表,`packages/cloud-connection/src/marketplace-install-local-plugin.ts` 里的 `asMarketplaceRegistration`,`packages/services/service-automation/src/plugin.ts` 里的 `nonHostDeclaringPackageOf`。 - **两分钟验证**:`pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/install-local-connector-package-ref.dogfood.test.ts`,预期 6 passed。 - **需要你做的**:不受管面,契约评审 PASS 后由席位落地,无需你操作。 ## What changes - **`packages/services/service-automation/src/plugin.ts`** — at the one call site that builds each entry's `ConnectorProviderContext`, `nonHostDeclaringPackageOf(entry, registry)` decides whose entry it is. It answers host only for: no registry; no stamp; a registry row whose `source` is not `'marketplace'`; or a residual owner that a `'code'` door recorded. Every other stamp is treated as installed or unrecorded, and that entry gets `createInstalledPackageRefRefusal` for both `loadPackageFile` and `resolvePackagePath`. The refusal names the entry, the package, the ref and the remedy, and never a host path. The classifier's docblock states what it trusts (the row the registering door wrote) and what it does not (the item's own stamp, the entry's stated source). - **`packages/objectql/src/engine.ts` / `plugin.ts`** (review must-fix M1) — the `registerApp` collection loop, nested plugins, seed datasets, `viewItems` and the manifest service's residual door all refuse an item whose body arrives with a `_packageId` other than the registering package's: `VALIDATION_ERROR`/400 at `_packageId`, the same envelope as the permission-set refusal. A stamp the registration itself wrote onto a caller's object is recorded and exempt. An install door always parses a fresh body, so an installed item can never use that exemption. `ObjectQL.recordResidualOwner` / `residualOwnerSource` record a residual owner with its door's source. - **`packages/cloud-connection/src/marketplace-install-local-plugin.ts`** (must-fix M2) — `asMarketplaceRegistration` registers every `packages[]` member as `'marketplace'`, at install and at rehydrate. The boot artifact's entry-over-door precedence is unchanged. - Docs: one row in the failure-modes table of `content/docs/automation/connectors.mdx`. Changesets: `@objectstack/objectql` minor, `@objectstack/cloud-connection` minor, `@objectstack/service-automation` patch. Each carries `Clause-②: no (narrowing)`, `Metadata-protocol: narrowed`, a BREAKING banner and `adr-0087: not-required (no-migration-prescription)`, in the shape of `.changeset/26711-view-write-door-unknown-key-refused.md`. ## Tests - Whole suites at `ffbdd0fb`: objectql 512 files / 9895 tests, cloud-connection 64 / 776, service-automation 250 / 3177, all passed. Typecheck exit 0 for objectql, cloud-connection, service-automation and dogfood. - New pins: - `collection-item-owner-stamp.test.ts` (12); - `marketplace-install-local-member-source.test.ts` (4); - the `#28534` block in `connector-materialization.test.ts` (8); - the dogfood file `install-local-connector-package-ref.dogfood.test.ts` (6), which covers a foreign self-stamp refused with 422 through the real door, and a two-member bundle whose member claims `source: 'code'` with its file ref still refused. - Ablations: M1 at `f8fa5255` (4 unit and 1 dogfood red, and the bypass reached `ready`); M2 at `7ea3d092` (3 unit and 1 dogfood red; the file is byte-identical since). - Dogfood battery (the `(narrowing)` arm) at `ffbdd0fb`, `pnpm --filter @objectstack/dogfood test`, whole package: 365 files passed, 3 skipped; 2940 tests passed, 28 skipped; no failure. ## Acceptance notes - Reviews: the boundary record FAILed at `2be3fd45` (6554878186, M1 and M2). The delta record FAILed at `f8fa5255` on the declaration only (6555831347, D1). This head re-declares, and the record for this head is pending. - Kept: an installed package's stdio `mcp` entries are refused, because the factory always asks `resolvePackagePath('.')`; an installed package reaches MCP over http. The reload path keeps the existing ADR-0097 policy: a configuration fault is logged at `error` and the entry is skipped, so the reason is in the reconcile log, not on `GET /automation/connectors`. - Carried elsewhere: authored and stored connector rows, #28594; connectors missing after a restart, #28591; the build-door judge for `_packageId`, #28627. - Open (D1 shape): the changesets use `not-required (no-migration-prescription)` with a prose remedy, as #26711 did. The gate accepts a FROM / TO table only beside a registered ADR-0087 ledger entry, which would be a new `packages/spec/src/migrations/entries/semantic/` file. That is left to this head's contract review. ## Verification <details><summary>Base measurement at a52191f (service-automation built from BASE's plugin.ts, same dogfood file)</summary> ```text FAIL the installed entry naming a host FILE does not materialize ... AssertionError: {"name":"pkgref_host_file","state":"ready","description":"Minimal OpenAPI document for the showcase's own health probe. ...","actions":["getHealth"]}: expected { name: 'pkgref_host_file', ...(7) } to be undefined FAIL the installed entry asking for a host LOCATION does not materialize ... AssertionError: {"name":"pkgref_host_cwd","state":"ready","description":"MCP connector exposing 1 tool(s) from a Model Context Protocol server.","actions":["echo_upper"]}: expected { name: 'pkgref_host_cwd', ...(7) } to be undefined Test Files 1 failed (1) Tests 2 failed | 2 passed (4) ``` Leg proof: tree blob of plugin.ts == BASE blob `a9bd5ae0`; `ablation-dist-preflight @objectstack/service-automation createInstalledPackageRefRefusal --absent`: marker absent from all 6 built files; restore: blob == HEAD `6aa82bd6`, `git diff HEAD` empty, rebuilt, preflight: marker present in 2 built files, working tree clean. </details> <details><summary>Suites: service-automation whole unit suite, typecheck, dogfood typecheck</summary> ```text 71597f1 pnpm --filter @objectstack/service-automation test Test Files 250 passed (250) · Tests 3175 passed (3175) (the merge of bca27ad that followed touched no service-automation file) 2be3fd4 pnpm --filter @objectstack/service-automation typecheck exit 0 (tsc + check:test-typecheck: 0 files / 0 errors in the debt ledger) 2be3fd4 pnpm --filter @objectstack/dogfood typecheck exit 0 (both also exit 0 at 2047273) 2047273 tsc --listFiles: dogfood program holds the new dogfood file (1), service-automation tsconfig.test.json holds connector-materialization.test.ts (1) ``` </details> <details><summary>After the fix: dogfood file and its error records (4 passed at 2047273, 71597f1 and 2be3fd4)</summary> ```text Test Files 1 passed (1) Tests 4 passed (4) ERROR [Automation] failed to materialize connector instance 'pkgref_host_file' via provider 'openapi' (ADR-0097). {"error":"connector-openapi provider: connector 'pkgref_host_file' failed to read providerConfig.spec './src/system/connectors/status-openapi.json': connector instance 'pkgref_host_file' is declared by package 'com.dogfood.pkgref', which was installed into this runtime and has no package root on its disk, so its package file ref './src/system/connectors/status-openapi.json' is refused: ... Rebuild the package with a current `os build`, ..."} ERROR [Automation] failed to materialize connector instance 'pkgref_host_cwd' via provider 'mcp' (ADR-0097). {"error":"connector instance 'pkgref_host_cwd' is declared by package 'com.dogfood.pkgref', ... so its package path ref '.' is refused: ..."} ``` </details> <details><summary>Ablation of the new check (once)</summary> `ablation-replace.mjs` anchor `return registry?.getPackage?.(packageId)?.source === 'marketplace' ? packageId : undefined;` replaced by `return void registry, undefined; /* ABLATED_28534 */`: anchor 1 -> 0, blob `6aa82bd6` -> `e71f702b`. The unit file imports `./plugin.js` from source, so no rebuild is in the path. `-t 28534`: 4 failed (every case that expects an installed entry refused) | 2 passed (the inline and host-entry controls). Restore: blob == HEAD `6aa82bd6`, `git diff HEAD` empty. </details> <details><summary>Local gates at 2be3fd4 (the derived set; CI runs the full farm)</summary> ```text node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; 5-path change set vs merge base bca27ad) -> 90 commands, each exit captured before any pipe first pass: 88 exit 0; 2 exit 3 PREREQUISITE NOT MET (dists of seven packages outside this diff): pnpm --filter @objectstack/spec run check:skill-examples -> built via turbo (44/44 cached) -> re-run exit 0 (279 prose examples type-check across 3 surfaces) pnpm check:dual-build-cjs-loads -> re-run exit 0 (115 require entry points across 70 packages load) dispatch-gates --ran (record with exit codes): 90 derived, 90 run, 0 NOT-MEASURED (a derived zero), 0 UNRUN pnpm --filter @objectstack/spec check:generated at 71597f1: all 14 generated artifacts up to date (the merge of bca27ad that followed touched no packages/spec file) ``` Not run locally, declared: `test:repo` (no repo-project test touched; CI's Test Core runs it); the full dogfood battery (no `!` in the title, `Clause-②: no`); `check-platform-checklist` (no export renamed or deleted; two exports added to `plugin.ts`, the package's `index.ts` unchanged). </details> --- _Generated by [Claude Code](https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d78a7e4 commit d2c4874

12 files changed

Lines changed: 1343 additions & 9 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/cloud-connection": minor
3+
---
4+
5+
fix(cloud-connection)!: every member of an installed bundle is recorded as installed, whatever its entry's `source` states
6+
7+
`POST /api/v1/marketplace/install-local` (and `os package install`) now registers every member of a `packages[]` bundle as `source: 'marketplace'`, at the install and at every restart's rehydrate. The `manifest` service stamps an entry's own `source` over the door's, so a member whose entry said `'code'` was recorded as the host's own code, and readers of a package's origin treated it so: the connector materializer let its package-relative file refs read the host's directory.
8+
9+
Clause-②: no (narrowing) — at the install-local door a `packages[]` entry's stated `source` no longer decides the member's recorded origin: a member stating `'code'` is recorded `'marketplace'`, as the door states. Nothing is widened.
10+
11+
Metadata-protocol: narrowed (an installed bundle member's `source: 'code'` is no longer honoured at the install door)
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No key is added, removed or renamed and no body is refused: the entry's `source` is still accepted, and still honoured where it means something, the boot of a merged artifact; at the install door the door's own statement wins, and it is the origin of every package this door registers. There is nothing for an author or a migration to rewrite. -->
14+
15+
**BREAKING** semantic narrowing at the install-local door (`@objectstack/cloud-connection`), shipped as `minor`.
16+
17+
- **Before.** A bundle installed through the door whose second member's entry stated `source: 'code'` recorded that member as `'code'`, and a ledger entry holding such a bundle did so again at every restart.
18+
- **Now.** Both members are recorded `'marketplace'`, at the install and at the rehydrate, so a ledger entry written earlier is recorded the same way after the next restart.
19+
- **Unchanged.** The `manifest` service's precedence for a boot that registers a merged artifact stating no source of its own: each entry's statement still stands there. An entry stating `'marketplace'` or nothing is handed through as received.
20+
- **Who sees it.** A bundle installed through this door whose entry claimed `'code'`. Its members now read as installed everywhere a package's origin is read.
21+
22+
**The remedy.** None is owed. Drop the entry's `source` if it was written for this door: the door states the origin of what it installs.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@objectstack/service-automation": patch
3+
---
4+
5+
fix(service-automation)!: an installed package's connector entry never reads the host's files
6+
7+
A connector entry declared by a package that was installed into the runtime (`os package install`, `POST /api/v1/marketplace/install-local`; the package registry's `source: 'marketplace'`) was handed `loadPackageFile` / `resolvePackagePath` anchored at the HOST's root, whichever package declared it. An installed package has no directory on the runtime it is installed into, so a relative ref resolved against another package's files: an installed `openapi` connector naming `./src/system/connectors/status-openapi.json` materialized `ready` with the showcase's own document, and a stdio `mcp` connector naming `./scripts/mcp-fixture.mjs` spawned the showcase's own fixture in the showcase's directory.
8+
9+
Clause-②: no (narrowing) — a connector entry that is not the host's own (an installed package's, or one whose stamp no registration records) is refused its package-relative file and path refs, which it was handed before against the host's root. Nothing is widened.
10+
11+
Metadata-protocol: narrowed (an installed package's connector `providerConfig` may no longer name a package-relative file, and an installed package's stdio `mcp` transport is refused)
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No key is added, removed or renamed and the stored shape is unchanged: the same `providerConfig` is accepted, and what is refused is the host's root being read on the installed package's behalf. No conversion can carry a file the install never held; the refusal names the entry, its package, the ref and the remedy, and a current `os build` already writes the document into the artifact. -->
14+
15+
**BREAKING** behaviour narrowing for installed packages' connectors (`@objectstack/service-automation`), shipped as `patch` beside the `minor` of `@objectstack/objectql` and `@objectstack/cloud-connection` that carries the same declaration.
16+
17+
- **Before.** An installed package's entry naming a package-relative file or location read the host's own file, or ran a stdio process in the host's directory.
18+
- **Now.** Both members refuse every ref for such an entry, naming the entry, its package and the ref: a configuration fault, so the instance is not materialized and the reconcile logs the refusal. Whose entry it is comes only from what a registering door recorded (the package row's `source`, or the residual owner's door for an artifact residual), never from the entry's own words: an entry stamped with a package no registration records is refused its refs as well.
19+
- **Unchanged.** An entry carrying its document inline or as an `http(s)` URL, and every entry of the host's own stack, which keeps reading its files as before.
20+
- **Who sees it.** An installed package whose connector names a package-relative `openapi` spec, or declares a stdio `mcp` transport.
21+
22+
**The remedy.** Rebuild the package with a current `os build`, which carries an `openapi` spec file into the artifact, or inline the document; an installed package reaches an MCP server over `http`, since a stdio transport needs a working directory inside the package.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"@objectstack/objectql": minor
3+
---
4+
5+
fix(objectql)!: a collection item never names its own owning package — package registration refuses a body `_packageId` other than the registering package's
6+
7+
`ObjectQL.registerApp` (a manifest and its nested plugins, seed datasets included) and the `manifest` service's ADR-0130 residual registration now refuse a collection item whose body already carries a `_packageId` other than the package registering it: `VALIDATION_ERROR` / 400 with `issues[]` at `_packageId`, before the item is filed, the envelope of the permission-set owner refusal in the same loop. The registration stamps `_packageId` only while it is unset, so a body could wear another package's id (an installed connector naming the host's own package, or an id nobody holds) and every reader of an item's owner, the connector materializer deciding whose directory a file ref resolves against among them, read the body's claim about itself.
8+
9+
Clause-②: no (narrowing) — package registration refuses a collection item whose body `_packageId` names a package other than the one registering it, which it registered before with the body's stamp kept. Nothing is widened.
10+
11+
Metadata-protocol: narrowed (a collection item's `_packageId`, declared by `MetadataProtectionFields`, may no longer name a package other than the one that registers it)
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No key is added, removed or renamed, and no value a legitimate producer writes is refused: the registration is the only writer of `_packageId` (`os build` writes none, the package export strips every underscore key, the artifact loader stamps with the very id the registration keys on), so a foreign stamp exists only in a body somebody wrote by hand to claim another owner. A D2 conversion could not tell such a stamp from an agreeing one without the registration that judges it, and the refusal names the item, both ids and the remedy. -->
14+
15+
**BREAKING** accept-set narrowing at package registration (`@objectstack/objectql`), shipped as `minor`.
16+
17+
- **Before.** A manifest `com.acme.app` whose connector carried `_packageId: 'com.acme.host'` registered the connector with that stamp kept, with no refusal and no warning; so did a nested plugin's item, a seed dataset and a multi-package artifact's residual item. `null` and `''` were kept too.
18+
- **Now.** The registration throws `400 VALIDATION_ERROR` with `issues[]` at `_packageId`, naming the item, the stamp and the package registering it, before anything of the item is filed. Through `POST /api/v1/marketplace/install-local` an inline install answers `422 PLUGIN_REGISTER_FAILED` with that message and persists nothing.
19+
- **Unchanged.** An absent `_packageId` and one equal to the registering package register as before. An item object that an earlier registration stamped in place (a module constant handed to a second registration) registers as it always has: only a stamp the object arrives with is judged. Objects (`objects`) are not judged by this seam.
20+
- **Who sees it.** Whoever hand-writes `_packageId` on a manifest item naming another package, which no build, export or install writes.
21+
22+
**The remedy.** Remove `_packageId` from the item. The package an item belongs to is the one that registers it, stamped by the registration.
23+
24+
The `manifest` service also records which door registered each multi-package artifact's residual owner (`ObjectQL.recordResidualOwner` / `residualOwnerSource`: `'code'` for the boot, `'marketplace'` for an install), the only record of that id's origin, since the residual has no package row.

‎content/docs/automation/connectors.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,7 @@ retry):
865865
| The upstream refuses the credential (`401` / `403`), or an `mcp` server sent a static credential does not list a tool the instance's `include` names | **Not fatal**: the instance registers **degraded**. A stored credential is marked `rejected` and waits until it is set again (or a restart); an environment or vault credential is retried on the backoff |
866866
| Instance `name` collides with a plugin-registered connector | **Hard boot error** — there is no silent precedence between the two worlds |
867867
| stdio transport without host opt-in | **Hard boot error** with the `declarativeStdio` fix inline — a security rejection is never retried |
868+
| An entry of an **installed** package (`os package install`, `POST /api/v1/marketplace/install-local`) names a package-relative file — an `openapi` `spec` path — or needs a location inside its package, as a stdio `mcp` transport's working directory does | Configuration fault, **never resolved against the host's directory**: an installed package has no directory on the runtime it was installed into, and the host's is not its own. The instance is not materialized, and the reconcile's error names the entry, its package and the ref. Rebuild the package with a current `os build`, which carries an `openapi` spec file into the artifact, or inline the document; reach an MCP server from an installed package over `http`. Whether an entry is the host's is read from what the install recorded, never from the package's own words: an entry carrying a `_packageId` other than its own package's is refused at the install, every member of a `packages[]` bundle is recorded as installed whatever its `source` says, and an entry naming a package no registration records is refused its ref too. The host's own entries keep reading their files |
868869
| MCP server unreachable / `tools/list` fails; remote spec URL unreachable or transiently failing (408/429/5xx) | **Not fatal**: the instance registers **degraded** (visible on `GET /api/v1/automation/connectors` with the reason), dispatches fail with that reason, and the platform retries on a 5s→5min backoff — recovery is automatic |
869870
| An `mcp` instance's `toolsSnapshot` does not match what the server serves (a pinned tool's description or `inputSchema` changed; or a pinned tool is no longer listed, on an instance whose credential did not reach the server) | **Not fatal**: the instance registers **degraded** with no actions, its reason naming each tool and the digest it now serves, and the logs say "serves a tool definition that differs from its toolsSnapshot" (`CONNECTOR_UPSTREAM_CHANGED`); it is re-checked on the same backoff and recovers once the server serves the pinned definitions again — or set the pin to the reviewed digest. A pinned tool no longer listed on an instance whose credential **did** reach the server (its `auth` over http) takes the credential row above instead: a server may hide tools from a credential, so the reason names both causes (fix the credential and set it again, or update the pin), a stored credential is marked `rejected`, and no retry runs against the same value |
870871
| A retry of a degraded instance reaches the upstream and meets a configuration fault (an `include` naming a tool or operation the upstream does not offer; on an `mcp` instance that sends a static credential, the credential row above applies instead) | **Not fatal**: the instance stays **degraded**, now with the configuration reason, and is **not retried** — dispatches fail saying to edit its declaration; an edit of the entry (or a restart) re-attempts it |

0 commit comments

Comments
 (0)