Skip to content

fix(service-automation,objectql,cloud-connection)!: an installed package's connector entry never reads the host's files - #28587

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-28534-installed-package-file-ref
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-28534-installed-package-file-ref

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

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

Verification

Base measurement at a52191f (service-automation built from BASE's plugin.ts, same dogfood file)
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.

Suites: service-automation whole unit suite, typecheck, dogfood typecheck
71597f1b  pnpm --filter @objectstack/service-automation test        Test Files 250 passed (250) · Tests 3175 passed (3175)
          (the merge of bca27ad3 that followed touched no service-automation file)
2be3fd45  pnpm --filter @objectstack/service-automation typecheck   exit 0 (tsc + check:test-typecheck: 0 files / 0 errors in the debt ledger)
2be3fd45  pnpm --filter @objectstack/dogfood typecheck              exit 0 (both also exit 0 at 2047273b)
2047273b  tsc --listFiles: dogfood program holds the new dogfood file (1), service-automation tsconfig.test.json holds connector-materialization.test.ts (1)
After the fix: dogfood file and its error records (4 passed at 2047273, 71597f1 and 2be3fd4)
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: ..."}
Ablation of the new check (once)

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.

Local gates at 2be3fd4 (the derived set; CI runs the full farm)
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack
  (no paths; 5-path change set vs merge base bca27ad3)  -> 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 71597f1b: all 14 generated artifacts up to date
  (the merge of bca27ad3 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).


Generated by Claude Code

…ve connector ref

An entry whose declaring package was installed (registry source
'marketplace') is handed refusing loadPackageFile / resolvePackagePath
members instead of ones anchored at the host's root.

Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U
Co-authored-by: Claude <noreply@anthropic.com>
…al; docs and changeset

Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cloud-connection, @objectstack/objectql, @objectstack/service-automation, touching 23 documentable anchor(s).

40 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 0e78922ee07bda70879a49793159804fa6b41b77.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 cross-cutting symbol(s) contributed no route anchor: packageId (4 routes)
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 79 pages)
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 214 client-bound route-ledger rows — the other 154 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 154: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 99 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 29 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 0e78922ee07bda70879a49793159804fa6b41b77 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6833018f5db92e0b528e9dfd0a41995297a7984c — the merge of head ffbdd0fbe1dcea2d6b0aacf5e72b0c2a589c87ff into base 0e78922ee07bda70879a49793159804fa6b41b77, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6833018f5db92e0b528e9dfd0a41995297a7984c && git checkout 6833018f5db92e0b528e9dfd0a41995297a7984c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0e78922ee07bda70879a49793159804fa6b41b77 ffbdd0fbe1dcea2d6b0aacf5e72b0c2a589c87ff && git checkout -B drift-repro 0e78922ee07bda70879a49793159804fa6b41b77 && git merge --no-ff ffbdd0fbe1dcea2d6b0aacf5e72b0c2a589c87ff

node scripts/docs-audit/affected-docs.mjs --json 0e78922ee07bda70879a49793159804fa6b41b77

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 0e78922ee07bda70879a49793159804fa6b41b77 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2be3fd45f4595b49ce7031091867d67e31350e13
Local-runs: none
Scope: packages/services/service-automation/src/plugin.ts, packages/services/service-automation/src/connector-materialization.test.ts, packages/qa/dogfood/test/install-local-connector-package-ref.dogfood.test.ts, content/docs/automation/connectors.mdx, .changeset/28534-installed-package-file-ref.md + packages/spec/src/integration/connector-provider.ts (loadPackageFile / resolvePackagePath TSDoc), packages/spec/src/integration/connector.zod.ts (ConnectorBaseSchema ... MetadataProtectionFields), packages/spec/src/stack.zod.ts (ArtifactPackageSourceSchema, ArtifactPackageSchema), packages/spec/src/kernel/package-registry.zod.ts (InstalledPackageSourceSchema), packages/spec/src/kernel/metadata-type-schemas.ts (connector: binding), packages/objectql/src/engine.ts (registerApp, stampPackage, the collection loop), packages/objectql/src/registry.ts (installPackage, registerItem), packages/objectql/src/plugin.ts (manifest service register), packages/spec/src/shared/protection.zod.ts (applyProtection), packages/core/src/artifact-packages.ts (resolveArtifactPackageEntries), packages/cloud-connection/src/marketplace-install-local-plugin.ts (inline install, rehydrate), packages/metadata-protocol/src/protocol-package-install.ts, packages/runtime/src/domains/packages.ts, packages/services/service-package/src/index.ts, packages/connectors/connector-mcp/src/mcp-provider.ts, packages/connectors/connector-openapi/src/openapi-provider.ts

Read at 2026-11-01T07:18Z, read-only: the diff bca27ad3..2be3fd45 (5 files), card #28534 (body, triage 6554177417, claim 6554218344, dev report 6554765334 as claims to verify), PR #28587 body, and the cited code at 2be3fd45. Nothing built or run. Check-runs on the head at read time: 36 success, 11 skipped, 10 still in progress (Build Core, Test Core shards 1, 2, 4 to 8, Type Check workspace and consumer gates), none failed.

① Derived judgments

  • The contract (connector-provider.ts ~:320-350) promises a ref resolved "against the root of the stack/package that declared the entry". The runtime at bca27ad3 handed every entry the host's root; the dev's base measurement of both members through the real install-local door is consistent with the code read. The premise holds.
  • The fix classifies an entry as installed by one reading: installedDeclaringPackageOf(entry, registry) is entry._packageId looked up in the objectql package registry, installed iff row.source === 'marketplace'. I verified the writers of source: install-local writes 'marketplace' at install (marketplace-install-local-plugin.ts:1951) and at rehydrate (:1022); ObjectQL.registerApp defaults 'code' (engine.ts:8782) for the host stack, each member of an artifact's packages[] and every plugin manifest; 'authored' is written only by registry.installPackage(manifest, 'authored', ...) from protocol-package-install.ts:310, runtime/src/domains/packages.ts:1697 and the sys_packages hydration (service-package/src/index.ts:777). The control-plane install (POST /api/v1/cloud-connection/install) only proxies to the cloud and registers nothing locally, so install-local is the only local door that writes 'marketplace'. That half of the classification is sound.
  • The other half is not: the stamp the classification reads, and the source the artifact entry may state, are both written by the installed package itself. Two reachable downgrades to the host's root, each through the card's own door, POST /api/v1/marketplace/install-local with an inline manifest, by reading (not measured, read-only shape):
    • (M1) A self-stamped _packageId. ConnectorBaseSchema spreads MetadataProtectionFields (connector.zod.ts:1055-1081), so _packageId is a declared, optional key on a connectors[] entry and survives any parse. The managed install path registers the body as received (normalizeBundle(inlineManifest) → convertPackageBodyForward → manifestService.register(converted, { source: 'marketplace' }) → registerApp); the collection loop registers each connector with stampPackage(toRegister, ownerId, ownerVersion) then registerItem(..., ownerId) (engine.ts:9135), and both stamp through applyProtection, which writes _packageId only while it is undefined (protection.zod.ts:237). Nothing refuses a body stamp that disagrees with ownerId for a connector (the ADR-0086 D3 refusal beside it covers permissions only). So an installed entry carrying _packageId: 'com.host.showcase' (a 'code' row) or _packageId: 'nobody' (no row) is classified as the host's and handed createPackageFileLoader(this.options.packageRoot): the measured cross-package read, one key away. The unit control "a stamp no row answers is the host's" pins the fail-open the spoof uses.
    • (M2) An artifact entry stating source: 'code'. install-local admits a body with packages[] (it reads manifest.packages itself at :1667-1670); the manifest service resolves entries through resolveArtifactPackageEntries, which keeps each entry's own source (artifact-packages.ts:358, ArtifactPackageSourceSchema admits 'code' and 'marketplace', stack.zod.ts:2010), and registers with source ?? options?.source (objectql/src/plugin.ts:944): the entry's word wins over the door's 'marketplace'. An installed bundle whose member states source: 'code' lands a 'code' row, and every connector it declares reads the host's root. The spec's docblock reserves that statement for a control plane merging installs into the boot artifact; the install-local door does not withhold it.
  • Fail-open on no registry / no row: no registry means no door could have written a row, which the code justifies. "A stamp no row answers = host" is the composed stack's residual owner (ADR-0130 D4) and is a real host case, but it is also what M1 exploits; it cannot stay a bare default once the stamp is the package's own word.
  • Both members: ConnectorProviderContext has exactly two package-anchored members (loadPackageFile, resolvePackagePath); the built-in openapi factory dereferences only the first (openapi-provider.ts:204), mcp only the second (mcp-provider.ts:351-352), and neither reads process.cwd() itself; the only process.cwd() default is inside resolveInsidePackageRoot (plugin.ts:315), which an installed entry no longer reaches. mcp-connector.ts:225 passes transport.cwd through, and providerConfig.transport carries no author-written cwd (the provider's own docblock). No third road found.
  • Reconcile timing: installPackage writes the row before the collection loop registers any item (registry.ts:6919-6931, then registerApp step 2+), and announceHotInstall triggers metadata:reloaded with { changed: [...] } only (:3942), so captureReloadedConnectors keeps no unstamped payload that could shadow the registry's stamped items. The materializer reads the registry object once per run but calls getPackage per entry, so the row is live. No ordering hole.
  • Q1 ('authored'): reachable today, outside this card's ruling. metadata-type-schemas.ts:272 binds connector to DeclarativeConnectorEntrySchema, which carries provider / providerConfig, so a stored row with providerConfig.spec: './x.json' is storable through the metadata door; the post-boot reconcile reads stored rows through readMetaItemsFromProtocol, which stamps each with its package_id (protocol-stored-overlay-reads.ts:165, an 'authored' row or none) and both fall to the host's root. Also reachable with no package at all (an environment-authored connector), which is the same class on a hosted tenant. A finding to file, not a must-fix for this head.
  • Q2 (every stdio mcp entry of an installed package refused): acceptable. The factory asks resolvePackagePath('.') unconditionally and the only alternative, no member at all, inherits the host's working directory (the measured reach by another road); the refusal and the docs name http as the road. Route B is not foreclosed.
  • Message: the refusal is thrown before any resolution and interpolates only the entry name, the declaring package id and the author's own ref; no host absolute path can appear (pinned by not.toContain(path.resolve(fixtureRoot)) in both unit cases). The openapi wrapper adds only its own prose. The cloud seat's finding 3 on [Epic] Marketplace and connector auto-shelf — E1 platform prerequisites (openapi/mcp hardening, outbound OAuth, request signing), E2 administrator credential hand-off, E3 author recipes and handbook, E4 the first shelf of official connector apps #27293 holds here.
  • Tests: the 6 unit cases and the 4 dogfood cases pin what the PR claims, including the host controls; the ablation the dev reports turns exactly the installed cases red. Nothing pins M1 or M2 because nothing in the design refuses them.
  • Docs row and changeset: accurate for the mechanism as built (door names, source: 'marketplace', os build remedy, http for MCP, host entries unchanged). The sentence "never resolved against the host's directory" overstates until M1 and M2 close.

② Semver level

@objectstack/service-automation patch, as declared. Behaviour narrows for one class of entry (an installed package's package-relative refs, and every stdio mcp entry of an installed package); no published schema or packages/spec member changes; Clause-②: no, Metadata-protocol: none are right.

③ Boundary flags

  • Boundary change: what an installed package's connector entry may read or launch on the host. The intended boundary (declaring package's root, never the host's) matches the contract text. The boundary as built is keyed on two inputs the installed package writes itself (M1, M2), so the installed package can opt out of it.
  • Fail-open defaults: no registry (justified), a stamp with no row (unsafe while the stamp is the package's own).
  • A hosted tenant never sees a host path in the refusal (verified).
  • No packages/spec change; the claim's file surface is respected. Both remedies below sit outside service-automation; the claim says stop and report on that, so the seat widens the claim or routes them, rather than the dev deciding alone.

④ Design judgment

The change holds against the scenario it was accepted on (an honest installed manifest naming a host file or location) and against the contract's words. It does not hold against the adversary the card names ("an installed package must not be able to bind another package's files by guessing a path"), because the classification trusts the package's own _packageId and its own artifact-entry source. Must-fix and should-fix, one line each:

  • must-fix M1: a connector (any collection item) registered under ownerId must not keep a body-carried _packageId that differs from ownerId: overwrite it, or refuse it the way permissionSetOwnerRegistrationRefusal refuses a permission set (engine.ts:9120-9135), and pin with an installed entry stamped _packageId of a 'code' package being refused its ref.
  • must-fix M2: an install-local install must register every packages[] member as 'marketplace' whatever the entry states (refuse or override source: 'code' at the install-local door or make the door's options.source win in objectql/src/plugin.ts:944 when the door states one), and pin it with a two-member inline bundle whose member states source: 'code'.
  • should-fix S1: drop the bare "a stamp no row answers is the host's" default: answer host only for the residual owner id the artifact names (or no stamp), refuse every other stamp no row answers, and retitle the unit control accordingly.
  • should-fix S2: file Q1 as a finding (class b): a stored or 'authored' provider-bound connector row reads the host's root through the same two members, reachable through the metadata door bound to DeclarativeConnectorEntrySchema.
  • should-fix S3: the installedDeclaringPackageOf docblock should state what it trusts (the registering door's row) and what it does not (the item's stamp, the entry's stated source) once M1 and M2 make that true.
  • should-fix S4: the dev's out-of-scope restart finding (installed connectors absent from GET /automation/connectors after rehydrate, inline entry included) is a real class a defect in a sibling lane; file it so it is not lost in a report comment.

Implemented-by: claude/issue-28534-installed-package-file-ref
Reviewed-by: session_017xRBcVBKAFk1ntXSjvPt7U

VERDICT: FAIL

…age classifier trusts only what a registering door wrote

registerApp and the residual door refuse a collection item whose body
_packageId differs from the package registering it; install-local
registers every packages[] member as 'marketplace'; the manifest
service records each residual owner's door; a stamp no registration
answers for is refused its package-relative refs.

Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U
Co-authored-by: Claude <noreply@anthropic.com>
… member source, and both bypasses over the real door; docs and changesets

Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U
Co-authored-by: Claude <noreply@anthropic.com>
…is not refused as the body's

registerItem and stampPackage stamp the caller's object in place, so a
module constant registered by one package and handed to another
arrived wearing the first registration's _packageId. Those stamps are
recorded where written and exempt; a stamp an object arrives with is
still refused.

Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f8fa5255294b87be28f44a39a6162c2f72845a5e
Local-runs: none
Scope: packages/objectql/src/engine.ts, packages/objectql/src/plugin.ts, packages/objectql/src/collection-item-owner-stamp.test.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/cloud-connection/src/marketplace-install-local-member-source.test.ts, packages/services/service-automation/src/plugin.ts, packages/services/service-automation/src/connector-materialization.test.ts, packages/qa/dogfood/test/install-local-connector-package-ref.dogfood.test.ts, content/docs/automation/connectors.mdx, .changeset/28534-installed-package-file-ref.md, .changeset/28534-item-owner-stamp-refused.md, .changeset/28534-install-local-member-source.md + packages/objectql/src/engine.ts (stampPackage, registerApp owner key manifest.id || manifest.name, registerMetadataCollections, METADATA_ARRAY_KEYS, seed data[], viewItems), packages/objectql/src/registry.ts (registerItem, installPackage), packages/spec/src/shared/protection.zod.ts (applyProtection), packages/spec/src/kernel/metadata-protection.zod.ts (MetadataProtectionFields._packageId), packages/spec/authorable-surface/integration.json, packages/core/src/artifact-packages.ts (artifactPackageId, resolveArtifactPackageEntries), packages/metadata/src/plugin.ts (_parseAndRegisterArtifact, registerArtifactBodyCollections, the manifest.register of the metadata objects), packages/cli/src/commands/serve.ts and packages/runtime/src/standalone-stack.ts (how a boot feeds the two registrars), packages/runtime/src/domains/packages.ts (POST /api/v1/packages, the export clean), packages/metadata-protocol/src/protocol-package-install.ts, packages/services/service-package/src/index.ts, packages/metadata-protocol/src/protocol-stored-overlay-reads.ts, packages/metadata-protocol/src/protocol.ts (tenantAuthoredWriteRefusal), scripts/check-adr-0087-registration.mjs (ruling A, CATEGORIES, findMigrationPrescription), scripts/check-changeset-no-major.mjs (the twelve level verdicts), AGENTS.md Post-Task 3, .claude/agents/os-dev.md (battery and declaration rules), the precedents eeb95f7 (objectql CHANGELOG) and .changeset/26711-view-write-door-unknown-key-refused.md

Read at 2026-11-01T08:59Z, read-only: the delta 2be3fd45..f8fa5255 (12 files, 3 commits), the prior record 6554878186, the claim amendment 6554888326, the dev's delta report 6555680458 as claims to verify, the PR body at the head, and the cited code at f8fa5255. Nothing built or run. Check-runs on the head at read time: 50 in all, 38 success, 12 skipped, 0 failed, none in progress; every required context green. The dogfood matrix shards are among the skipped, so no dogfood file has run on this head in CI; the dogfood readings are the dev's local ones.

① Derived judgments

  • M1 as built. collectionItemOwnerStampRefusal (engine.ts) refuses an item whose body carries a _packageId that is not undefined and not ownerId, in VALIDATION_ERROR/400 with issues[] at _packageId, before anything is filed; called in registerMetadataCollections (the manifest seam and every nested plugin run one body), on each seed data[] entry, on each viewItems body, and at the manifest service's residual door (registerUnclaimedTopLevelItems, both the container branch and the generic branch, after the name and permission-set judges). null and '' fall to the refusal because applyProtection writes _packageId only while it is undefined (protection.zod.ts), so either would otherwise survive as an owner no row answers. Refuse-not-overwrite matches the two sibling refusals of the same loop and their recorded reason. Verified against the code and against the ten unit pins and the dogfood M1 case (422 PLUGIN_REGISTER_FAILED through the real door, no ledger entry, the host's file-ref entry still ready).
  • The exemption is sound. stampPackage and registry.registerItem both stamp the CALLER's object in place (applyProtection "always mutates"), so an object registered once wears that registration's stamp for life; noteRegistrationStamp records that write in a process-wide WeakMap keyed by object identity, and the refusal exempts only REGISTRATION_WRITTEN_STAMPS.get(item) === stamped. For an install-door body to be exempt it would have to be the very object an earlier registration stamped. Every install door parses fresh: inline req.json() → normalizeBundle → convertPackageBodyForward → asMarketplaceRegistration → register; the managed path from the fetched payload; the rehydrate from the ledger file. A structuredClone or JSON round trip of a stamped object leaves the map and is refused (pinned: "a fresh copy wearing the same stamp is refused"), so the exemption only ever widens acceptance for the identical object, never for a value. The other registrar never meets it: MetadataPlugin._parseAndRegisterArtifact stamps in place with artifactPackageId(body) (= id || name, the same key registerApp uses) or the residual's unclaimed.ownerId, but files through the MetadataManager sink, not registerApp, and the boots that run both (serve.ts, standalone-stack.ts) hand the artifact door a file path and the AppPlugin its own object, so no stamped item object reaches registerApp. A WeakMap holds its keys weakly (no retention), is process-wide on purpose (the measured red was a module constant stamped by another engine in the same process), and its only history dependence is "an object a registration already stamped is accepted again", which no door can arrange. A narrower structural rule exists, stamping a copy instead of the caller's object, but it moves every consumer of the in-place stamp and is not owed on this head.
  • Door coverage. install-local inline, managed and rehydrate → manifest.register → registerApp: covered. Code stacks, plugin manifests, the apps' manifest.register(...): covered by the same loop. The residual door: covered. POST /api/v1/packages, the protocol's installPackage and the sys_packages hydration write an 'authored' row through registry.installPackage and register no collection item (no registerApp/registerItem in those files), so M1 has nothing to judge there; stored rows reach the registry through registerItem with the row's package_id (protocol-stored-overlay-reads.ts), the S2 lane. objects is not in METADATA_ARRAY_KEYS, so an object body's _packageId is not judged by this seam; an object carries no package-file ref, so nothing of this card rides on it. Note, not a finding.
  • M2 as built. asMarketplaceRegistration restates every top-level packages[] entry whose source is stated and not 'marketplace', on a copy, by reference when nothing changes; applied at the two and only two manifestService.register calls of the file (install :1987, rehydrate :1056). resolveArtifactPackageEntries reads the top-level array only and never recurses, so the top-level restatement is complete. objectql/src/plugin.ts:944 (source ?? options?.source) is byte-unchanged and package-source-stamp.test.ts is not in the diff: the boot artifact's entry-over-door precedence is untouched, as the claim amendment allowed. Pinned over a real engine (both rows 'marketplace', install and a pre-rule ledger entry) and through the real door (dogfood M2: 200, the member's file ref refused naming the member and "installed into this runtime", its inline sibling ready).
  • S1/S3 as built. nonHostDeclaringPackageOf: no readers → host; no stamp → host; a row → 'marketplace' is installed, any other source is host; no row → a residual owner a 'code' door recorded is host, one a 'marketplace' door recorded is installed; anything else (no record, or a non-string or empty stamp) is unrecorded and refused. recordResidualOwner is written by the manifest service with options?.source ?? 'code' before anything of the residual registers; only a door that states no source writes 'code' (the boot's manifest.register), install-local always states 'marketplace', and a recorded 'marketplace' never downgrades. A row is consulted before the residual record, so an installed package whose id collides with the host's residual owner reads installed, not host. I found no way for an installed package to get its id recorded as a 'code' residual. The dev's acknowledged fail-closed side effect (a stored connector row stamped with a package no registration holds is now refused its refs) is the right direction and is in the S2 lane. The docblock states what it trusts and does not; accurate.
  • The exemption is applied unevenly. noteRegistrationStamp follows registerItem at the collection loop and the seed loop, not at the viewItems site (engine.ts, after the new refusal there), although that site stamps the caller's object in place the same way. Harmless in production (no door re-registers a viewItems body under another id); a fixture shared across tests would be refused there and exempt one site up. Should-fix.
  • Legitimate producers of a differing body stamp — none found. The package export strips every _ key and PROVENANCE_KEYS (packages.ts clean); os build/compile stamps nothing (no applyProtection or _packageId write under packages/cli/src); the artifact loader stamps with the owner registerApp keys on and never hands that object to registerApp; the install ledger persists the body with stamps equal to the ids the rehydrate registers under (member → member id, residual → bundle id); the apps' module-constant manifests register under one fixed id each (equal stamp, or the map); seed bodies served by the package door are applied as rows, not re-registered; no stack under examples/ or committed JSON carries _packageId. The pre-existing wrong-owner case (one object registered under A, then handed to B's registration) is kept as it was, which the dev notes; it is not reachable through a door.
  • Tests and ablations. The unit, cloud-connection and service-automation pins and the two dogfood cases pin what the delta claims; the dev's M1 ablation at this head and M2 ablation at 7ea3d092 (file byte-identical at the head) turn exactly the new cases red, as reported. Not re-run here.
  • Docs row and changesets. Accurate for the mechanism, with one wording nit: the docs row says "an entry carrying its own _packageId is refused at the install"; the refused case is a _packageId other than its package's — an equal stamp registers (pinned CONTROL). The objectql changeset's "null and '' are refused", "seed datasets are judged the same way" and the residual-owner record all match the code; the cloud-connection changeset's door-not-service reasoning matches; the service-automation changeset's added sentence matches S1.

② Semver level

The declaration is wrong after M1, and this is the must-fix. _packageId is an authorable key: MetadataProtectionFields (metadata-protection.zod.ts) is spliced into each metadata schema "to expose the optional protection envelope to its TS authors", ConnectorBaseSchema spreads it, and the ledger lists integration/Connector:_packageId and integration/DeclarativeConnectorEntry:_packageId in packages/spec/authorable-surface/integration.json. defineStack/os validate accept any string there; before this head registerApp accepted and honoured it, and now refuses every value but the registering package's. That is "a value refused" in AGENTS.md Post-Task 3: Metadata-protocol: narrowed. This repo has declared the same shape the same way twice: the sibling refusal in this very loop (ADR-0086 D3, commit eeb95f7: fix(spec,objectql)!:, Clause-②: yes (narrowing), an adr-0087: disposition marker, a BREAKING banner, minor) and .changeset/26711-view-write-door-unknown-key-refused.md (fix(spec)!:, Clause-②: no (narrowing), Metadata-protocol: narrowed, minor). Ruling A as mechanised in check-adr-0087-registration.mjs makes narrowed a breaking carrier in its own right and has the level gate refuse narrowed beside a Clause-②: line with no (narrowing) arm, so the two travel together. What the PR must declare:

  • PR body, line-initial: Clause-②: no (narrowing) (nothing is widened, no new key) and Metadata-protocol: narrowed; plus the third line a kind other than none owes, Skills-docs: ..., written from the diff (no skills/** file teaches _packageId on a connector; the docs row in connectors.mdx is the only teaching, so none — REASON or follow-up owed — FILES is the honest spelling unless the package's evals exercise the docs row).
  • Every changeset body: the same two lines line-initial. @objectstack/objectql → minor (the package carrying the narrowing; a kind takes at least minor on a moved package). @objectstack/cloud-connection → minor (M2 changes what an entry's stated source means at the install door, a semantic kind of its own, folded into narrowed by "several ⇒ narrowed"); @objectstack/service-automation may stay patch (the level gate's discharged verdict: one moved package at minor+, the others patch). Each changeset body that carries the breaking declaration carries exactly one adr-0087: HTML-comment disposition marker (registered id or not-required (category ...), the gate prints CATEGORIES) and the migration prescription in a shape findMigrationPrescription reads (an arrow rewrite or a FROM / TO pair: FROM a connector or any collection item carrying _packageId naming another package, TO the key removed; the package an item belongs to is the one that registers it).
  • Title: ! — fix(objectql,cloud-connection,service-automation)!: ... — the repo's spelling for a declared break (both precedents carry it; no gate forces the bang, the arm does the mechanical work, and the os-dev battery rule treats the two alike).
  • The (narrowing) arm owes the full dogfood battery on the final head (pnpm --filter @objectstack/dogfood test, no shard, no path, recorded as dogfood_battery); it has not run on this head anywhere (the PR run skips the dogfood matrix).
  • The served tier stays CONTRACT_REVIEW_TIER (narrowing face). The claim's two lines (Clause-②: no, Metadata-protocol: none) are the seat's to re-declare; the dev copied them as the rule says.

③ Boundary flags

  • Boundary change: whose root a connector entry's package-relative refs resolve against is now keyed only on what a registering door wrote (the row's source, or the residual owner's door), never on the entry's own _packageId or an artifact entry's stated source. M1 and M2 close both opt-outs over the real door. Verified.
  • Fail-open defaults left: no objectql service (justified), no stamp (an item registered into no package), 'authored' rows (the S2 lane, filed separately). New fail-closed: a stamp no registration answers for is refused.
  • Declared versus enforced, one document at two doors: the stack schema (defineStack, os validate, os build, the artifact door's parse) still accepts a connector _packageId naming another package that the boot now refuses with a 400 — the asymmetry the loop's own comment and the ADR-0086 D3 precedent closed by putting one judge in packages/spec and running it at both doors. packages/spec is outside this claim: a card to file, not a must-fix for this head.
  • The refusal messages carry no host absolute path (unchanged, pinned).
  • File surface: within the widened claim; packages/spec untouched. The declared tier face moves from schema to narrowing with the re-declaration; same tier.

④ Design judgment

The change now holds against the adversary the card names: an installed package can no longer opt out of its classification by stamping _packageId (refused at the door, in the loop's existing envelope) or by stating source: 'code' on a bundle member (the door's word wins at install and rehydrate, the boot artifact's precedence untouched), and a stamp nobody's registration answers for fails closed. The exemption for registration-written stamps is identity-keyed and unreachable from any door; acceptable. What fails this head is its declaration: the diff is a declared-surface narrowing, this repo's own precedents declare exactly that, and the current none/no/patch/no-! set states the opposite, with the full dogfood battery the arm owes not yet run.

  • must-fix D1: re-declare per ②: PR body Clause-②: no (narrowing) + Metadata-protocol: narrowed + a Skills-docs: line; the same two lines in all three changesets, @objectstack/objectql and @objectstack/cloud-connection at minor (service-automation may stay patch), each with exactly one adr-0087: disposition marker and a FROM / TO migration prescription; title with !; then the full dogfood battery on the final head, recorded.
  • should-fix S5: file the build-door follow-up (out of claim, packages/spec): one judge for a collection item's _packageId against its owning body, run at defineStack/os validate/os build and the artifact parse as ADR-0086 D3's judgePermissionSetOwnerBinding is, so the document the boot refuses is refused where it is written.
  • should-fix S6: apply noteRegistrationStamp after registerItem at the viewItems site too (engine.ts, beside the new refusal there), or record in the docblock why that site stamps in place without recording it.
  • should-fix S7: docs row wording: "an entry carrying a _packageId other than its own package's is refused at the install" (an equal stamp registers).
  • should-fix S8: the PR body still describes the first head (What changes, Tests, the Acceptance note "a stamp no row answers ... read as the host's, exactly as today"); it must be rewritten for M1/M2/S1/S3 before landing — the seat's edit, per the dev's open question.

Implemented-by: claude/issue-28534-installed-package-file-ref
Reviewed-by: session_017xRBcVBKAFk1ntXSjvPt7U

VERDICT: FAIL

…ctql and cloud-connection at minor

Clause-2 no (narrowing) and Metadata-protocol narrowed, one adr-0087
disposition each. Pins the viewItems channel stamping its parse output
and words the docs row as 'a _packageId other than its own package's'.

Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot changed the title fix(service-automation): an installed package's connector entry never reads the host's files fix(service-automation,objectql,cloud-connection)!: an installed package's connector entry never reads the host's files Nov 1, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head ffbdd0fbe1dcea2d6b0aacf5e72b0c2a589c87ff · 2026-11-01T10:18Z

Taken under references/contract-review.md (b): this head has waited a full triage round with no record, claim or hand-off on it. Face narrowing, so the review runs at CONTRACT_REVIEW_TIER. The previous FAIL (6555831347) names an earlier head, so this review judges whether its must-fix findings are closed on this head. If the earlier reviewer's session posts a record on this head first, that record governs and this claim is withdrawn.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ffbdd0fbe1dcea2d6b0aacf5e72b0c2a589c87ff
Local-runs: none
Scope: packages/services/service-automation/src/plugin.ts, packages/services/service-automation/src/connector-materialization.test.ts, packages/objectql/src/engine.ts, packages/objectql/src/plugin.ts, packages/objectql/src/collection-item-owner-stamp.test.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/cloud-connection/src/marketplace-install-local-member-source.test.ts, packages/qa/dogfood/test/install-local-connector-package-ref.dogfood.test.ts + packages/spec/src/integration/connector-provider.ts (loadPackageFile / resolvePackagePath TSDoc), packages/spec/src/shared/protection.zod.ts (applyProtection), packages/spec/src/kernel/package-registry.zod.ts (InstalledPackageSourceSchema), packages/spec/src/stack.zod.ts (ArtifactPackageSourceSchema), packages/objectql/src/registry.ts (installPackage, getPackage), packages/objectql/src/index.ts, packages/services/service-automation/src/index.ts, packages/cloud-connection/src/index.ts, packages/core/src/artifact-packages.ts (resolveArtifactPackageEntries), packages/connectors/connector-openapi/src/openapi-provider.ts, packages/connectors/connector-mcp/src/mcp-provider.ts, packages/metadata-protocol/src/protocol-stored-overlay-reads.ts, packages/metadata-protocol/src/protocol.ts (getMetaItemsForExecution), packages/metadata/src/plugin.ts (unclaimedTopLevel, _parseAndRegisterArtifact), packages/runtime/src/app-plugin.ts, packages/runtime/src/standalone-stack.ts, packages/cli/src/commands/serve.ts (the AppPlugin wrap), packages/cli/src/commands/package/install.ts, scripts/check-adr-0087-registration.mjs (CATEGORIES, the prescription refusal), scripts/check-changeset-no-major.mjs (judgeLevel), AGENTS.md Post-Task Checklist step 3, .changeset/26711-view-write-door-unknown-key-refused.md, and the prose faces read for ②: .changeset/28534-installed-package-file-ref.md, .changeset/28534-item-owner-stamp-refused.md, .changeset/28534-install-local-member-source.md, content/docs/automation/connectors.mdx

Read at 2026-11-01T10:40Z, read-only: the net diff origin/main...ffbdd0fb (12 files, merge-base bca27ad3), card #28534 (body and every comment: triage, claim and two amendments, three dev reports as claims to verify, the two earlier records 6554878186 and 6555831347, the triage claim 6556639455), PR #28587 (body, file list, comments, no review comments), and the cited code at ffbdd0fb. Nothing built or run. This is the first record on this head. It judges the earlier records' must-fixes on this head and gives the whole narrowing face a fresh read.

Check-runs on ffbdd0fb at read time: 54 in all, 39 success, 14 skipped, 1 failure, 0 in progress. The failure is Check Changeset of the PR Automation run started 09:06Z (run 41291630233): check-changeset-no-major.mjs read the PR body's then-current Clause-②: no beside minor on two packages and refused it as narrowed-without-arm. The same job ran again on the same head at 09:10Z after the body's re-declaration (run 41291974120) and is success. The red is this PR's and is stale: it names a body that no longer exists; the newest run of the same check on this head is green. It is not main's red: PR Automation runs on pull requests only, and the merge-base bca27ad3 carries one cancelled Packed-tarball smoke (release candidate) and nothing else non-green. Main's p0 #28622 (Dogfood Regression Gate 1/3, route parity on GET /packages/:id/customizations) does not appear here because the PR run skipped the dogfood matrix shards (filter decided). So no dogfood file has run on this head in CI; the dogfood readings below are the dev's local ones (dogfood file 6 passed; the whole battery 2940 passed, 28 skipped, at this head).

① Derived judgments

Each accept-set or public-surface change the diff implies, named right or wrong.

  1. service-automation, the materializer (plugin.ts). For a connector entry whose declaring package is not the host's, both loadPackageFile and resolvePackagePath are createInstalledPackageRefRefusal: every relative ref throws before any resolution. Refused now, accepted before: an installed openapi entry whose providerConfig.spec is a file path (it read the host's file), and every stdio mcp entry of an installed package, whatever its command, because mcp-provider.ts:351 asks resolvePackagePath('.') unconditionally (it ran in the host's directory). Doors: the fatal boot reconcile (throws, boot fails) and the reload reconcile (logged at error, entry skipped). Sentence: connector instance 'X' is declared by package 'P', which was installed into this runtime and has no package root on its disk, so its package file ref './x' is refused: ... with the os build / inline / http remedy. The openapi wrapper prefixes failed to read providerConfig.spec. No host path is interpolated (pinned twice). Right. Neither refused input was legitimate under the contract: connector-provider.ts:344 resolves "against the root of the stack/package that declared the entry", and an installed package has no root here. Inline documents and http(s) URLs never reach a member (pinned). Host entries keep createPackageFileLoader(this.options.packageRoot) with its process.cwd() default (plugin.ts:315), unchanged.
  2. Who is "not the host's" (nonHostDeclaringPackageOf). No registry service, or no _packageId on the entry: host. A row for the stamp: 'marketplace' is installed, any other source ('code', 'authored') is host. No row: a residual owner the manifest service recorded as 'code' is host, one recorded 'marketplace' is installed. Anything else (no row, no record, or a non-string or empty stamp) is unrecorded and refused. Right, and fail-closed where it must be. I verified the writers of source: registerApp defaults 'code' (engine.ts:8907) and refuses 'authored' by type; install-local writes 'marketplace' at install (:1987) and rehydrate (:1056); 'authored' comes only from registry.installPackage(..., 'authored') in the metadata-protocol and runtime package doors, which register no collection item. The row is written before any item (installPackage precedes the collection loop), and getPackage is a live map read. The 'authored' and stored-row origins are connector materializer: a stored or authored provider-bound connector row resolves its package-relative providerConfig refs against the HOST's root (the #28534 class, through the metadata door) #28594's lane; this PR does not claim it.
  3. objectql, registerApp and the residual door (engine.ts, plugin.ts). A collection item (every METADATA_ARRAY_KEYS collection, a nested plugin's items, seed data[], viewItems) and every residual item of a multi-package artifact is refused when its body arrives with a _packageId that is not undefined and not the registering package's id: VALIDATION_ERROR / 400, issues[] at _packageId, the permission-set owner refusal's envelope, before anything is filed. null and '' fall to it because applyProtection (protection.zod.ts:237) overwrites only undefined. Install-local surfaces it as 422 PLUGIN_REGISTER_FAILED and persists no ledger entry (pinned over the real door). Exempt: a stamp a registration of this process wrote onto the very same object (REGISTRATION_WRITTEN_STAMPS, a WeakMap keyed by identity, filled only when the stamp was undefined before). Right. The exemption admits nothing by value: a JSON round trip of a stamped object is refused (pinned), and every install door parses its body fresh. The viewItems site stamps parsed.data, never the caller's entry (pinned on this head), so its lack of a noteRegistrationStamp is correct, and the comment says why (closes S6). objects is not in the loop and carries no file ref. Legitimate input broken: none found. An equal stamp registers (pinned). A module constant re-registered under its own id registers (pinned). No producer in this repo writes a foreign stamp: os build stamps nothing, the package export strips underscore keys, the artifact door stamps with artifactPackageId(body) or unclaimed.ownerId, the same ids the manifest service keys on.
  4. objectql, ObjectQL.recordResidualOwner / residualOwnerSource. The manifest service records the residual owner's door (options?.source ?? 'code') before the residual registers (plugin.ts:963-964); 'marketplace' never downgrades. Two additive public methods on the exported ObjectQL class. Right, and not a metadata accept-set change: nothing an author writes moves. collectionItemOwnerStampRefusal, nonHostDeclaringPackageOf, createInstalledPackageRefRefusal and asMarketplaceRegistration are module exports not re-exported from any package index.ts (read: service-automation re-exports AutomationServicePlugin, createPackageFileLoader only; objectql ObjectQL, ObjectQLPlugin, ...; cloud-connection the plugin class and its config type). Not a published-surface change.
  5. cloud-connection, asMarketplaceRegistration. At both manifestService.register calls of install-local (install and rehydrate) every top-level packages[] entry whose source is stated and not 'marketplace' is restated 'marketplace', on a copy. Refused now, honoured before: a bundle member's source: 'code' at the install-local door. Right. ArtifactPackageSourceSchema's docblock reserves the statement for the producer that merged a boot artifact; every package this door registers came through this door. resolveArtifactPackageEntries reads the top-level array only (artifact-packages.ts:256-330, declared.map, no recursion), so the top-level restatement is complete. objectql/src/plugin.ts:945 (source ?? options?.source) is byte-unchanged: the boot artifact's entry-over-door precedence stands. A ledger entry written before this rule is covered at the next rehydrate (pinned over a real engine).
  6. Docs row and three changesets (prose faces, read for ②). The connectors.mdx row names the doors, the refusal class, the remedy and the three ownership rules; the S7 wording ("a _packageId other than its own package's") is applied. Each changeset's Before / Now / Unchanged / Who sees it matches the code. One omission, should-fix S10 below.

Must-fixes of the two earlier records on this head: M1 (self-stamped _packageId) closed by item 3; M2 (member source: 'code') closed by item 5; D1 (the declaration) closed as judged in ②. Should-fixes S1, S3, S6, S7 closed; S2 is #28594, S4 is #28591, S5 is #28627; S8 stays open (below). The PR body claims Fixes #28534 only; no Closes/Fixes reference to #28594 or #28627 anywhere in the body, the commits or the diff.

② Semver level

Three changesets, read on the tree: @objectstack/objectql minor, @objectstack/cloud-connection minor, @objectstack/service-automation patch. Each carries, line-initial, Clause-②: no (narrowing) and Metadata-protocol: narrowed, a fix(...)!: summary, a BREAKING banner with Before / Now / Unchanged / Who sees it, a prose remedy, and exactly one adr-0087: marker, not-required (no-migration-prescription) with its reason. The PR body carries the same two lines plus Skills-docs: none — ... with a reason, and the title carries !. check-adr-0087-registration.mjs reads all three as BREAKING+bang+clause-②-narrowing+metadata-protocol-narrowed and passes; check-changeset-no-major.mjs passes on the re-run (the discharged lane: a kind declared, a moved package at minor).

The kind is right. _packageId is on the authorable surface through MetadataProtectionFields, and registerApp now refuses every value of it but the registering package's: "a value refused" is narrowed (AGENTS.md step 3). Nothing is widened, so no (narrowing). minor on objectql and cloud-connection follows.

D1's FROM / TO half. The earlier record asked for a FROM / TO prescription beside the adr-0087: marker. On the tree that pair is impossible in the shape it asked for: CATEGORIES tolerates a prescription only beside registered / already-registered (a ledger entry in packages/spec), and refuses no-migration-prescription when the body carries one, statement against statement. AGENTS.md owes a FROM → TO mapping for "removing or renaming anything an author can write"; this diff removes and renames nothing. It refuses a value no in-repo producer writes, and the remedy is one sentence, present in each body. That is the .changeset/26711-view-write-door-unknown-key-refused.md shape exactly. So the marker as pushed is the honest disposition, and I judge D1 closed. A ledger entry, if the maintainer wants one for the at-rest case (a hand-written ledger body carrying a foreign stamp, refused at the next rehydrate), belongs with #28627's spec-side judge; the dev's open question on it is answered in ③.

One level nit, should-fix S9: the service-automation changeset grades patch while its own body declares Metadata-protocol: narrowed. The gate's discharged lane accepts one moved package at minor with the others at patch, and the previous record allowed it. AGENTS.md's sentence "a kind takes at least minor on a package the diff moves" reads otherwise. Grade it minor, or the seat records why patch stands. Not a FAIL: the gate is the mechanised ruling and it is green.

③ Boundary flags

④ Design judgment

The narrowing holds against the published spec on main: it makes the spec's own words real at the consumer. connector-provider.ts:344 promises the declaring package's root; the materializer handed every entry the host's. An installed package has no root on this disk, so the only honest answer is a refusal that names the entry, the package and the ref, and that is what both members now throw. It takes no declared capability away from anyone the contract served: an inline document and an http(s) URL still work for an installed package, and the host's entries read their own files as before.

Per file on the narrowing scope:

  • service-automation/src/plugin.ts: holds. The classifier trusts door-written state only and fails closed on an unanswered stamp; both members are replaced, not dropped; the refusal takes the existing configuration-fault channel; the readers are read once per run with live lookups; getService('objectql') failures fall to the host path exactly as readDeclaredConnectorItems does. I tried the three roads the brief names. Other origins: an 'authored' or stored row still reads the host root (connector materializer: a stored or authored provider-bound connector row resolves its package-relative providerConfig refs against the HOST's root (the #28534 class, through the metadata door) #28594, not this card); an environment-authored row with no package is unstamped and reads the host root (same lane). Rehydrate after restart: the rehydrate rewrites every member row 'marketplace' (M2 applied there too) and re-registers items whose in-place stamps equal the ids it registers under, so the refusal holds at the next reconcile; that no reconcile runs until an unrelated reload is install-local: after a restart an installed package's connector never materializes — the kernel:ready rehydrate runs after automation's boot reconcile and announces no reload #28591, which this PR neither causes nor fixes. Host-root fallbacks: packageRoot undefined falls to process.cwd() for host entries only; an installed entry never reaches resolveInsidePackageRoot; transport.cwd is not author-written.
  • service-automation/src/connector-materialization.test.ts: 8 cases pin the two refusals, the message, the inline control, the three host controls (a code row, a code-recorded residual, no stamp), the unrecorded refusal, the installed residual, and one refused beside one serving. The fakes model getPackage and residualOwnerSource as the plugin reads them.
  • objectql/src/engine.ts: holds. Refuse-not-overwrite matches the two sibling refusals of the loop and their recorded reason. The identity-keyed exemption is the narrowest rule that keeps the in-repo fixtures green and admits nothing by value. The viewItems site is judged on the parsed body and documented.
  • objectql/src/plugin.ts: holds. The residual record is written before the residual registers, with the door's source; the residual door refuses in the body seam's envelope at the right place in the order.
  • objectql/src/collection-item-owner-stamp.test.ts: 12 cases pin the envelope, null/'', nested plugin, seed data, the equal-stamp and re-registration controls, the identity exemption against a JSON copy, the viewItems site, and the residual door with its owner record over a real kernel.
  • cloud-connection/src/marketplace-install-local-plugin.ts: holds. The door states what it registers; the boot artifact's precedence is untouched; a copy is made only when something changes.
  • cloud-connection/src/marketplace-install-local-member-source.test.ts: 4 cases over a real engine and manifest service: install, pre-rule ledger rehydrate, and the helper's copy/by-reference behaviour.
  • qa/dogfood/test/install-local-connector-package-ref.dogfood.test.ts: 6 cases over the real install-local door into the showcase, M1 and M2 included, each with the host controls. Local reading only on this head (CI skipped the matrix).

No must-fix. Should-fixes, one line each:

Implemented-by: claude/issue-28534-installed-package-file-ref
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U

席位核对

ACCEPT at head ffbdd0fb · Implemented-by: claude/issue-28534-installed-package-file-ref · Reviewed-by: this seat (lane ACCEPT at TIER_DEFAULT) + the CONTRACT_REVIEW_TIER record PASS 6556805066 on this same head (narrowing face, by triage#1, taken under contract-review (b)). Earlier records: FAIL 6554878186 (2be3fd45, M1/M2) and FAIL 6555831347 (f8fa5255, D1), both closed on this head per the PASS record.

Check Result
Shape Fixes #28534 is the only closing keyword. The body opens with the seat's signature line, then Clause-②: no (narrowing), Metadata-protocol: narrowed and Skills-docs: none — …. The title carries !.
Scope 12 files, +1343/−9. Not governed (check-governed-merges --pr 28587). Changesets: objectql minor, cloud-connection minor, service-automation patch, each declaring the narrowing and adr-0087: not-required (no-migration-prescription).
Evidence Whole suites at this head: objectql 9895, cloud-connection 776, service-automation 3177, all passing. Dogfood battery for the (narrowing) arm: 2940 passed, 28 skipped, 0 failed. One ablation each for M1 and M2.
Gates 54 check-runs. The one failure is the stale 09:06Z Check Changeset, which read the pre-re-declaration body; the 09:10Z re-run on this head is success. check-expected-skips --pr 28587: OK (16 skipped, all in main's roster). Mergeable clean.
Body Rewritten by the seat for this head before the PASS was posted. It no longer contains the fail-open sentence (a stamp no row answers gives 0 hits), and it names nonHostDeclaringPackageOf. S8 in the PASS record was read against the earlier body and is closed by that rewrite.

Should-fixes S9 to S12 of the PASS record, the seat's call:

Landing: not governed. The seat lands it now (pr_ready + automerge_enable).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 10:38
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 10:38
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit d2c4874 Nov 1, 2026
58 of 59 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-28534-installed-package-file-ref branch November 1, 2026 10:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

1 participant