Skip to content

fix(cloud-connection): an install-local install connects and records the package's declared datasources - #28132

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-aercb9
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-aercb9

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #28039

Clause-②: yes (widening)
Metadata-protocol: none

Skills-docs: content/docs/data-modeling/external-datasources.mdx gains one callout under "When auto-connect fails" (an installed package's datasources are registered as code and connected at install and every restart; a failed one is installed with its failed status; the install answer's datasources; 409 on a held name). skills/**: none — git grep -n -i "datasource" skills/objectstack-platform/references/operations.md has no line on install-local and datasources; the install-local rows there (:107, :344) describe the CLI and the credential facts, which this change does not touch.

维护者速读

  • 改了什么:通过 marketplace 本地安装(os package install)装进运行中环境的包,现在会像启动其产物时一样,把它声明的数据源注册为代码数据源、记入代码数据源名单并自动连接;卸载时一并撤回。启动路径与安装路径共用一个 runtime 助手 connectPackageDatasources。
  • 为什么改:此前安装只注册对象、不管数据源,绑定到包自带数据源的对象读写一律 404 OBJECT_NOT_FOUND,后台也不把它当代码数据源。
  • 风险与回滚:启动路径行为不变(既有 53 个数据源启动测试 + runtime 全套 7883 通过);新增两处对外行为:安装答复多一个 datasources 字段,包声明的数据源名若已被占用(default、后台建的、别的包的)则安装 409 拒绝。回滚即还原本 PR。
  • 先看:packages/runtime/src/declared-datasources.ts(助手本体,重点看 installed 分支吞掉 fail-fast 改为回报状态)、packages/cloud-connection/src/marketplace-install-local-plugin.ts(安装 4b′、重新加载、2a 冲突拒绝)、packages/runtime/src/app-plugin.ts(启动路径改为调用助手)。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run test/install-local-declared-datasources.dogfood.test.ts,预期 8 个用例全绿:安装后绑定对象可写可读,凭据无法解析的数据源读到 503 ERR_DATASOURCE_UNAVAILABLE,卸载后全部撤回。
  • 需要你做的:无,席位落地。凭据从哪来仍由 datasource: the credential refusals send a code author (defineDatasource) to a Setup form that cannot edit a code-defined datasource; name a binding path a code author can follow #27687 裁决,本 PR 不涉及。

What changes

  • One path. AppPlugin.start's three datasource steps — the in-memory metadata registration as origin: 'code' stamped with the declaring package, the connectDeclared call through the datasource-connection service, and its fail-fast verdict — move into connectPackageDatasources (packages/runtime/src/declared-datasources.ts, exported from @objectstack/runtime). The boot calls it with the same lists by reference (finding: AppPlugin hands the RAW artifact bundle to job scheduling and datasource registration, so a retired duration key (jobs[].timeout, turso config.timeout, memory config.persistence.autoSaveInterval) is silently dropped #23029's identity pins hold), the same log lines and the same throw; its names still join the code-datasource set in init().
  • install-local calls it on install (step 4b′, after the schema sync, only when the package hot-loaded) and on every kernel:ready rehydrate, with installed: true, which differs from the boot in three ways: the names are contributed to the code-datasource set now; a fail-fast verdict is logged and reported instead of thrown (the datasource keeps its retained verdict and the engine's unavailable mark, so a bound read answers 503 ERR_DATASOURCE_UNAVAILABLE naming it); and a reinstall first withdraws what the previous version registered.
  • Uninstall. The helper registers, once per kernel, the runtime.package-datasources uninstall cleanup on the protocol's registry (the shape runtime.package-jobs uses), so install-local's DELETE and the protocol's deletePackage both close the pools, remove the metadata registrations still stamped with the package and take the names out of the set. It acts only on what an installed package's call recorded; a boot artifact records nothing.
  • The install answer carries datasources: [{ name, status }] — the connection service's own ConnectStatus names, nothing new — omitted for a package that declares none.
  • Step 2a refusal. A package declaring a datasource name another holder owns (the host's default, a datasource in the metadata service not stamped with this package) is refused 409 RESOURCE_CONFLICT with details.datasource, before any write: the connect is idempotent on the name, so it would have served the package's objects from the holder's pool, and recording the name as code would have made the holder's own datasource read-only.
  • CodeDatasourceNames gains delete, and code-datasource-names.ts a withdrawCodeDatasourceNames (module-internal, not exported from the package); the module header now names the post-boot producer.

Tests

  • packages/qa/dogfood/test/install-local-declared-datasources.dogfood.test.ts (new, real showcase composition, real sqlite driver): install answers both verdicts; registrations are origin: 'code' with _packageId/_packageVersion; the code-datasource set holds both names; the admin list serves qads_store ok and qads_vault error with a statusReason naming the credential; write 201 + read 200 of the bound object; the vault-bound read 503 ERR_DATASOURCE_UNAVAILABLE; uninstall cleanups carries runtime.package-datasources { success: true, removed: 2 } and leaves no verdict, registration or name (control: showcase_external stays); showcase_external and default collisions 409; a restart's rehydrate reconnects and the object reads.
  • packages/runtime/src/declared-datasources.test.ts (new): the installed/boot split — names recorded or not, fail-fast reported or thrown, reinstall withdrawal, cleanup scoped to its package and to registrations still stamped with it, no connection service.

Acceptance notes

  • Measured on main 549203d0 first (same dogfood fixture, source reverted): install 200 with hotLoaded: true, then POST/GET /data/qads_note and GET /data/qads_secret_row all 404 OBJECT_NOT_FOUND ("Object 'qads_note' is not registered"); no connection verdict for either datasource; the code-datasource set held only default and showcase_external.
  • The unresolved-credential status. With an object bound explicitly the connection service's fail-fast path throws, and connect() retains that verdict as failed-degraded (reason naming the credential); without a bound object it is failed-credentials. Both are the service's existing names, and the admin list maps both to error. Recording failed-credentials on the fail-fast path too would be a change to service-datasource's retained verdict on a refused boot; not made here.
  • datasource: the credential refusals send a code author (defineDatasource) to a Setup form that cannot edit a code-defined datasource; name a binding path a code author can follow #27687 is not ruled. The credential resolves the one way the connection service resolves every credentialsRef. The installed datasources now pass the round-3 gate datasource: the credential refusals send a code author (defineDatasource) to a Setup form that cannot edit a code-defined datasource; name a binding path a code author can follow #27687 measured (trigger: 'declared-auto' and CODE_DATASOURCE_NAMES_SERVICE.has(name)), so whatever it rules for code datasources reaches them.
  • datasourceMapping rules in an installed package are not installed by this door (before or after); the helper is called with no mapped objects, so D2 (d) never fires for an installed package.
  • objectos artifact-kernel-factory (outside this repo) composes its own kernels; whether it connects package datasources itself is not measured here.
  • Out of scope, reported to the seat: the 503 text for a failed datasource (DatasourceUnavailableError, packages/objectql/src/driver-connect-errors.ts) says it "failed to connect at startup and the server was started with OS_ALLOW_DRIVER_CONNECT_FAILURE", which is false for an install-time failure (measured on this PR's dogfood read of qads_secret_row) and, by the same branch, for a Setup-created datasource that failed.

Verification

Ablations — each leg red, then restored (git diff HEAD empty)

All through scripts/ablation-replace.mjs (anchor 1 → 0, blob moved) under the verify lock. The dogfood file resolves @objectstack/cloud-connection and @objectstack/runtime to source (the main-side measurement above ran on reverted source with no rebuild), so no dist preflight applies.

Leg Mutation Result
A1 install step 4b′ connect → undefined dogfood 6 failed / 2 passed
A2 rehydrate connect removed dogfood 1 failed (rehydrate)
A3 2a refusal disabled dogfood 1 failed (409 collisions)
A4 uninstall cleanup registration removed dogfood 1 failed (uninstall withdrawal)
A5 if (!installed) throw err → throw err unit 1 failed (fail-fast reported)
Local runs at head 475a5292
  • Measurement on main 549203d0 (dogfood fixture, source reverted, then restored): Tests 1 passed, readings in Acceptance notes.
  • pnpm --filter @objectstack/dogfood exec vitest run test/install-local-declared-datasources.dogfood.test.ts — Tests 8 passed (8).
  • pnpm --filter @objectstack/runtime test — Test Files 484 passed (484), Tests 7883 passed | 19 skipped (7902).
  • pnpm --filter @objectstack/cloud-connection test — Test Files 61 passed (61), Tests 758 passed (758).
  • pnpm --filter @objectstack/runtime typecheck, pnpm --filter @objectstack/cloud-connection typecheck — exit 0 (after pnpm --filter @objectstack/runtime build).
  • node scripts/pm/dispatch-gates.mjs --commands: 92 families, each run with its exit captured before any pipe; three exited 3 (prerequisite) on the first pass — check-plugin-teardown-shape --self-test (cleared by fetching the pinned commit), spec check:skill-examples and check:dual-build-cjs-loads (cleared by building client-react, studio and five plugin/service packages) — and exited 0 on re-run. --ran: 92 derived famil(ies) accounted for — 92 run, 0 NOT-MEASURED.
  • pnpm lint — exit 0.
  • Dogfood battery: n/a (no ! in the title, no (narrowing) arm). test:repo is CI's.

Generated by Claude Code

…the package's declared datasources

The install door registered a package's objects and nothing of its
datasources, so an object bound to one answered 404 OBJECT_NOT_FOUND and
the name was not code-defined. AppPlugin's registration, code-set record
and connectDeclared call move into one runtime helper,
connectPackageDatasources, which the boot keeps calling and install-local
now calls on install and on every rehydrate. In a running kernel a
fail-fast verdict is reported (the datasource answers its retained
verdict and a bound read answers 503 ERR_DATASOURCE_UNAVAILABLE) instead
of thrown; a reinstall replaces, and the uninstall withdraws through the
runtime.package-datasources cleanup. A package declaring a name another
holder owns is refused 409 before any write.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cx4xgJksGo7iVA8c9A
…hangeset for install-local datasources

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

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cloud-connection, @objectstack/runtime, touching 28 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/runtime/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

17 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 062a2323ba363d15f507433987cca4d650979b40.

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/runtime/src/index.ts) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: packageId (4 routes)
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 32 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 062a2323ba363d15f507433987cca4d650979b40 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from eb28a88492c50d939abd7af927ca7ab00b978620 — the merge of head dbc6f440b9aaf1b38d1d6c39442dbae91fd73683 into base 062a2323ba363d15f507433987cca4d650979b40, 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 eb28a88492c50d939abd7af927ca7ab00b978620 && git checkout eb28a88492c50d939abd7af927ca7ab00b978620
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 062a2323ba363d15f507433987cca4d650979b40 dbc6f440b9aaf1b38d1d6c39442dbae91fd73683 && git checkout -B drift-repro 062a2323ba363d15f507433987cca4d650979b40 && git merge --no-ff dbc6f440b9aaf1b38d1d6c39442dbae91fd73683

node scripts/docs-audit/affected-docs.mjs --json 062a2323ba363d15f507433987cca4d650979b40

⚠️ 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 062a2323ba363d15f507433987cca4d650979b40 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…narrowing

The install-local door now refuses a package whose declared datasource
name the environment already holds; main installed that package 200, so
the changeset declares the narrowing arm with its BREAKING banner and
ADR-0087 disposition.

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

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head 9ae8fc1682699ce3aeef51ebd726cb4151070018 · 2026-10-30T13:43Z

The lane seat runs the CONTRACT_REVIEW_TIER review this PR owes (Clause-②: yes (narrowing): the changeset at this head declares it; the PR body's yes (widening) predates the seat's finding that the held-name 409 narrows install-local, and the relay cannot edit a PR body, so the record carries the correction).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9ae8fc1682699ce3aeef51ebd726cb4151070018
Local-runs: none
Scope: packages/runtime/src/declared-datasources.ts, packages/runtime/src/app-plugin.ts, packages/runtime/src/code-datasource-names.ts, packages/runtime/src/index.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/runtime/src/declared-datasources.test.ts, packages/qa/dogfood/test/install-local-declared-datasources.dogfood.test.ts + packages/services/service-datasource/src/datasource-connection-service.ts (connectDeclared, disconnect, getConnectionState), packages/services/service-datasource/src/datasource-admin-service.ts (createDatasource), packages/services/service-datasource/src/datasource-admin-plugin.ts (putDatasourceRecord, boot restore), packages/metadata/src/metadata-manager.ts (get, unregister, registerInMemory), packages/runtime/src/app-artifact-handlers.ts (runtime.package-jobs cleanup shape), packages/cli/src/commands/package/install.ts, ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts, scripts/check-changeset-no-major.mjs, scripts/pm/clause2-line.mjs, scripts/check-adr-0087-registration.mjs

Declaration at this head: the changeset .changeset/28039-install-local-declared-datasources.md declares fix(cloud-connection)!:, Clause-②: yes (narrowing), Metadata-protocol: none, a BREAKING banner and adr-0087: not-required (no-migration-prescription). That is the declaration this review judges. The PR body's Clause-②: yes (widening) line and the title without ! are SUPERSEDED by the changeset: this PR widens (new @objectstack/runtime export connectPackageDatasources + 4 types; new optional datasources member on the install-local answer) AND narrows (a package declaring a datasource name the environment already holds is refused 409 RESOURCE_CONFLICT, details.datasource, where main answered 200). The relay cannot edit the body; the corrected reading is Clause-②: yes (narrowing), Metadata-protocol: none, BREAKING, !.

① Derived judgments

  • Widening 1 — connectPackageDatasources + DeclaredDatasourceOwner, OwnedDatasources, PackageDatasourceOptions, PackageDatasourceVerdict exported from @objectstack/runtime (index.ts). Correct: @objectstack/cloud-connection resolves it by await import('@objectstack/runtime') with a logged fallback when the runtime predates it, so a package-level export is required; it cannot stay module-internal. withdrawCodeDatasourceNames and CodeDatasourceNames.delete stay unexported from the package, as the body says. Judged right.
  • Widening 2 — install-local answer gains datasources: [{ name, status }], omitted (not emptied) when the package declares none, so the answer's key set for every existing caller is unchanged (same shape as credentialRefs, connectors: sys_connector_credential + open-tier resolver chain (env → credential object → vault), reconcile on credential change, install-local prompt for missing credentialRefs (ruling A on #27230) #27237). status values are the connection service's existing ConnectStatus names, no new vocabulary. Judged right.
  • Narrowing — step 2a findHeldDatasourceName: literal default → refused; else metadata.get('datasource', name) with _packageId !== manifestId → refused. Covers the host's default, the boot artifact's (in-memory, stamped), Setup-created (putDatasourceRecord → metadata.register('datasource', …) at create, and re-registered at boot restore, origin: 'runtime') and another installed package's (registered in memory by the helper). Placed after the user-code MANIFEST_CONFLICT check and before step 2b, which the existing code pins as "BEFORE hot-register and before anything is written to the ledger"; nothing is written before it. A reinstall/upgrade of the SAME package passes (owner === manifestId skips); the kernel:ready rehydrate does not run 2a at all, so a restart never refuses a ledgered package. Judged right; two fail-open edges noted in ③.
  • Boot path (Q1): AppPlugin.start is behaviour-identical for consumers. Registration + connect now run through the helper with installed unset: no withdraw, no name contribution (phase-1 contribution at app-plugin.ts:626 is untouched), ownership stamping via owned: () => this.codeDefinedDatasourceOwners(ctx) resolved inside the same lenient warn catch as before, the same datasources list by reference (finding: AppPlugin hands the RAW artifact bundle to job scheduling and datasource registration, so a retired duration key (jobs[].timeout, turso config.timeout, memory config.persistence.autoSaveInterval) is silently dropped #23029), the same three log lines, and the fail-fast verdict re-thrown so boot is refused. Differences, none contractual: registerDeclaredApiEndpoints now runs after the connect instead of between registration and connect (it reads definition.apis only); the datasource collection is converted once instead of twice; a throw from convertedCollection/resolveMappedObjects itself now propagates without the [AppPlugin] declared-datasource auto-connect failed log line (it still refuses boot).
  • Held-name check completeness (Q2): complete for every holder the metadata service knows. Not seen: a driver registered only on the engine with no metadata registration (the legacy onEnable escape hatch) — the install then proceeds and the verdict surfaces as already-registered in datasources, not silently. Acceptable; noted in ③.
  • Uninstall/reinstall (Q3): withdrawPackageDatasources calls connection.disconnect(name) (closes the pool, drops the retained verdict, clears the engine's unavailable mark, evicts the driver — datasource-connection-service.ts), unregisters the metadata item only when still stamped with this package, withdraws the names from the set and clears the WeakMap entry. The cleanup is registered once per protocol under runtime.package-datasources on the same registerUninstallCleanup registry runtime.package-jobs uses, so both the install-local DELETE and the protocol's deletePackage run it. INSTALLED_BY_APP is a WeakMap keyed by the kernel's CodeDatasourceNames instance (created before kernelKey is read), DATASOURCE_CLEANUP_REGISTERED a WeakSet keyed by the protocol: no cross-kernel leak. A reinstall withdraws first (unit test pins [STORE, VAULT] → [STORE]). Judged right.
  • Unresolved credential: connectDeclared attempts every datasource, then throws (fatal aggregation); the helper on installed logs, reads back each retained verdict via getConnectionState and returns it; install still answers 200; bound read 503 ERR_DATASOURCE_UNAVAILABLE (dogfood). The diff does not decide where a code datasource's credential comes from: it hands external.credentialsRef to the connection service's existing resolver (datasource: the credential refusals send a code author (defineDatasource) to a Setup form that cannot edit a code-defined datasource; name a binding path a code author can follow #27687 untouched, stated in the module header and the PR). Judged right.
  • Tests: unit file pins the installed/boot split (names recorded or not, thrown vs reported, reinstall withdrawal, cleanup scoped to the package and to registrations still stamped with it, no connection service). Dogfood pins both verdicts on the answer, origin: 'code' + _packageId/_packageVersion, code-set membership, admin list ok/error, write+read through the driver, 503, uninstall cleanups entry { success: true, removed: 2 }, 409 for showcase_external and default with nothing reaching the set or the connection service, and the rehydrate after restart. Adequate for the acceptance scenario.

② Semver level

  • Both entries patch with a BREAKING banner and ! under Metadata-protocol: none: correct per check-changeset-no-major.mjs LEVEL AXIS (ruling A): the level is decided only by the Metadata-protocol: kind; a non-protocol narrowing "ships patch and keeps its BREAKING banner and its ADR-0087 disposition". No authorable key, value, default or type moves here.
  • Clause-②: yes (narrowing) is a recognised spelling (clause2-line.mjs:113: widens one surface and narrows another) and is the true one: the arm is what the ADR-0087 gate and the queue guard read.
  • adr-0087: not-required (no-migration-prescription): justified. No spec key, Zod schema, stored shape or exported declaration is removed or narrowed (only added); nothing for objectstack migrate meta to rewrite; the changeset body carries no FROM→TO rewrite or migration table (the "remedy" sentence is a naming choice, not a prescription), so the category's one mechanical refusal does not fire.
  • Changeset prose checked against the diff: "Before/Now/Unchanged/Who sees it", "one path", "install answer carries datasources … omitted for a package that declares none", "credential that cannot be resolved … named non-ok verdict … 503", "reinstall replaces … uninstall closes pools, removes registrations and names, reported as runtime.package-datasources in cleanups" — all true. One overstatement: "the ADR-0062 D2 gate … apply as on a boot" — D2 (d) (datasourceMapping routing) never fires for an installed package because install-local passes no mappedObjects (the PR body says so); the gate's (a)(b)(c) legs apply as on a boot.
  • Docs callout (external-datasources.mdx): every sentence true against the diff (registered origin: code, read-only, auto-connected at install and every restart, failed status kept, 503 ERR_DATASOURCE_UNAVAILABLE, datasources on the answer, 409 on a held name).

③ Boundary flags

④ Design judgment

  • Narrowing against the published spec on main: install-local's accept set on main admitted a package whose declared datasource name was already held and silently dropped the declaration (200, hotLoaded: true, objects 404). With the datasources now registered as code and connected, admitting such a package would serve its objects from the holder's pool (the connect's idempotency guard) and make the holder's own datasource read-only to the admin door — the refusal makes the spec's own invariant (one holder per datasource name; code names are read-only) real at the consumer rather than taking a capability away. 409 RESOURCE_CONFLICT with details.datasource matches the door's existing refusal shape (namespace/base conflict). Holds.
  • The shared helper: one path for boot and install with three explicit installed deltas (names contributed now, fail-fast reported not thrown, withdraw-before-register + uninstall cleanup) is the right altitude; the boot path keeps its exact behaviour and the install path reuses the connection service's retained verdicts rather than inventing a status. The owned thunk preserves AppPlugin's "ownership resolution inside the lenient catch" invariant. Holds.
  • Public surface: the 4 exported types are the helper's parameter/return types, needed for a typed cross-package call; nothing else is exported. The datasources member is the minimal answer a CLI/agent needs to see a failed credential at install time without a second call. Holds.
  • Clients (Q7): ui/.../marketplaceApi.ts types the managed answer as an open TS interface (an added optional member is additive); CLI os package install handles unknown non-ok statuses with the generic printError of the body's error.message (the 409 reads as a named refusal); no spec Zod schema for the install-local answer exists to strict-parse it. Nothing breaks.

Implemented-by: claude/project-thread-aercb9
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

…pe helper, keeping the plugin's class name

A static method called by the class's own name made esbuild rename the
shipped class `_MarketplaceInstallLocalPlugin`, so the CLI's
install-local identity no longer matched it. The two datasource methods
also move above the handler binder's docblock, which they had split from
its method.

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

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head dbc6f440b9aaf1b38d1d6c39442dbae91fd73683 · 2026-10-30T14:07Z

The lane seat re-runs the CONTRACT_REVIEW_TIER review this PR owes (Clause-②: yes (narrowing)) at the CI-fix head; the PASS 6524451070 at 9ae8fc16 is void for queueing.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: dbc6f440b9aaf1b38d1d6c39442dbae91fd73683
Local-runs: none
Scope: packages/runtime/src/declared-datasources.ts, packages/runtime/src/app-plugin.ts, packages/runtime/src/code-datasource-names.ts, packages/runtime/src/index.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/runtime/src/declared-datasources.test.ts, packages/qa/dogfood/test/install-local-declared-datasources.dogfood.test.ts + packages/services/service-datasource/src/datasource-connection-service.ts (connectDeclared, disconnect, getConnectionState), packages/services/service-datasource/src/datasource-admin-service.ts (createDatasource), packages/services/service-datasource/src/datasource-admin-plugin.ts (putDatasourceRecord, boot restore), packages/metadata/src/metadata-manager.ts (get, unregister, registerInMemory), packages/runtime/src/app-artifact-handlers.ts (runtime.package-jobs cleanup shape), packages/cli/src/commands/package/install.ts, ui/packages/app-shell/src/console/marketplace/marketplaceApi.ts, scripts/check-changeset-no-major.mjs, scripts/pm/clause2-line.mjs, scripts/check-adr-0087-registration.mjs

Declaration at this head: the changeset .changeset/28039-install-local-declared-datasources.md declares fix(cloud-connection)!:, Clause-②: yes (narrowing), Metadata-protocol: none, a BREAKING banner and adr-0087: not-required (no-migration-prescription). That is the declaration this review judges. The PR body's Clause-②: yes (widening) line and the title without ! are SUPERSEDED by the changeset: this PR widens (new @objectstack/runtime export connectPackageDatasources + 4 types; new optional datasources member on the install-local answer) AND narrows (a package declaring a datasource name the environment already holds is refused 409 RESOURCE_CONFLICT, details.datasource, where main answered 200). The relay cannot edit the body; the corrected reading is Clause-②: yes (narrowing), Metadata-protocol: none, BREAKING, !.

① Derived judgments

  • Delta 9ae8fc16..dbc6f440 (CI fix, supersedes record 6524451070 for queueing): one file, marketplace-install-local-plugin.ts, 54/54 lines. private static declaredDatasources becomes the module-scope function declaredDatasources (identical body: list, or map keyed by name → { name, ...def }), its two callers (connectDeclaredDatasources, findHeldDatasourceName) now call it unqualified, and the #21321 docblock that the previous head had left orphaned above the static is restored above bindArtifactHandlers. No remaining MarketplaceInstallLocalPlugin. self-reference in the file, so esbuild no longer renames the shipped class to _MarketplaceInstallLocalPlugin (the cause of the CLI serve-capability-identity / serve-marketplace-offline-install reds). No behaviour change; the cloud-connection diff vs main is now 158 pure additions; changeset, runtime files, docs and tests are byte-identical to the previous head. Every judgment below, made on 9ae8fc16, stands unchanged.

  • Widening 1 — connectPackageDatasources + DeclaredDatasourceOwner, OwnedDatasources, PackageDatasourceOptions, PackageDatasourceVerdict exported from @objectstack/runtime (index.ts). Correct: @objectstack/cloud-connection resolves it by await import('@objectstack/runtime') with a logged fallback when the runtime predates it, so a package-level export is required; it cannot stay module-internal. withdrawCodeDatasourceNames and CodeDatasourceNames.delete stay unexported from the package, as the body says. Judged right.

  • Widening 2 — install-local answer gains datasources: [{ name, status }], omitted (not emptied) when the package declares none, so the answer's key set for every existing caller is unchanged (same shape as credentialRefs, connectors: sys_connector_credential + open-tier resolver chain (env → credential object → vault), reconcile on credential change, install-local prompt for missing credentialRefs (ruling A on #27230) #27237). status values are the connection service's existing ConnectStatus names, no new vocabulary. Judged right.

  • Narrowing — step 2a findHeldDatasourceName: literal default → refused; else metadata.get('datasource', name) with _packageId !== manifestId → refused. Covers the host's default, the boot artifact's (in-memory, stamped), Setup-created (putDatasourceRecord → metadata.register('datasource', …) at create, and re-registered at boot restore, origin: 'runtime') and another installed package's (registered in memory by the helper). Placed after the user-code MANIFEST_CONFLICT check and before step 2b, which the existing code pins as "BEFORE hot-register and before anything is written to the ledger"; nothing is written before it. A reinstall/upgrade of the SAME package passes (owner === manifestId skips); the kernel:ready rehydrate does not run 2a at all, so a restart never refuses a ledgered package. Judged right; two fail-open edges noted in ③.

  • Boot path (Q1): AppPlugin.start is behaviour-identical for consumers. Registration + connect now run through the helper with installed unset: no withdraw, no name contribution (phase-1 contribution at app-plugin.ts:626 is untouched), ownership stamping via owned: () => this.codeDefinedDatasourceOwners(ctx) resolved inside the same lenient warn catch as before, the same datasources list by reference (finding: AppPlugin hands the RAW artifact bundle to job scheduling and datasource registration, so a retired duration key (jobs[].timeout, turso config.timeout, memory config.persistence.autoSaveInterval) is silently dropped #23029), the same three log lines, and the fail-fast verdict re-thrown so boot is refused. Differences, none contractual: registerDeclaredApiEndpoints now runs after the connect instead of between registration and connect (it reads definition.apis only); the datasource collection is converted once instead of twice; a throw from convertedCollection/resolveMappedObjects itself now propagates without the [AppPlugin] declared-datasource auto-connect failed log line (it still refuses boot).

  • Held-name check completeness (Q2): complete for every holder the metadata service knows. Not seen: a driver registered only on the engine with no metadata registration (the legacy onEnable escape hatch) — the install then proceeds and the verdict surfaces as already-registered in datasources, not silently. Acceptable; noted in ③.

  • Uninstall/reinstall (Q3): withdrawPackageDatasources calls connection.disconnect(name) (closes the pool, drops the retained verdict, clears the engine's unavailable mark, evicts the driver — datasource-connection-service.ts), unregisters the metadata item only when still stamped with this package, withdraws the names from the set and clears the WeakMap entry. The cleanup is registered once per protocol under runtime.package-datasources on the same registerUninstallCleanup registry runtime.package-jobs uses, so both the install-local DELETE and the protocol's deletePackage run it. INSTALLED_BY_APP is a WeakMap keyed by the kernel's CodeDatasourceNames instance (created before kernelKey is read), DATASOURCE_CLEANUP_REGISTERED a WeakSet keyed by the protocol: no cross-kernel leak. A reinstall withdraws first (unit test pins [STORE, VAULT] → [STORE]). Judged right.

  • Unresolved credential: connectDeclared attempts every datasource, then throws (fatal aggregation); the helper on installed logs, reads back each retained verdict via getConnectionState and returns it; install still answers 200; bound read 503 ERR_DATASOURCE_UNAVAILABLE (dogfood). The diff does not decide where a code datasource's credential comes from: it hands external.credentialsRef to the connection service's existing resolver (datasource: the credential refusals send a code author (defineDatasource) to a Setup form that cannot edit a code-defined datasource; name a binding path a code author can follow #27687 untouched, stated in the module header and the PR). Judged right.

  • Tests: unit file pins the installed/boot split (names recorded or not, thrown vs reported, reinstall withdrawal, cleanup scoped to the package and to registrations still stamped with it, no connection service). Dogfood pins both verdicts on the answer, origin: 'code' + _packageId/_packageVersion, code-set membership, admin list ok/error, write+read through the driver, 503, uninstall cleanups entry { success: true, removed: 2 }, 409 for showcase_external and default with nothing reaching the set or the connection service, and the rehydrate after restart. Adequate for the acceptance scenario.

② Semver level

  • Both entries patch with a BREAKING banner and ! under Metadata-protocol: none: correct per check-changeset-no-major.mjs LEVEL AXIS (ruling A): the level is decided only by the Metadata-protocol: kind; a non-protocol narrowing "ships patch and keeps its BREAKING banner and its ADR-0087 disposition". No authorable key, value, default or type moves here.
  • Clause-②: yes (narrowing) is a recognised spelling (clause2-line.mjs:113: widens one surface and narrows another) and is the true one: the arm is what the ADR-0087 gate and the queue guard read.
  • adr-0087: not-required (no-migration-prescription): justified. No spec key, Zod schema, stored shape or exported declaration is removed or narrowed (only added); nothing for objectstack migrate meta to rewrite; the changeset body carries no FROM→TO rewrite or migration table (the "remedy" sentence is a naming choice, not a prescription), so the category's one mechanical refusal does not fire.
  • Changeset prose checked against the diff: "Before/Now/Unchanged/Who sees it", "one path", "install answer carries datasources … omitted for a package that declares none", "credential that cannot be resolved … named non-ok verdict … 503", "reinstall replaces … uninstall closes pools, removes registrations and names, reported as runtime.package-datasources in cleanups" — all true. One overstatement: "the ADR-0062 D2 gate … apply as on a boot" — D2 (d) (datasourceMapping routing) never fires for an installed package because install-local passes no mappedObjects (the PR body says so); the gate's (a)(b)(c) legs apply as on a boot.
  • Docs callout (external-datasources.mdx): every sentence true against the diff (registered origin: code, read-only, auto-connected at install and every restart, failed status kept, 503 ERR_DATASOURCE_UNAVAILABLE, datasources on the answer, 409 on a held name).

③ Boundary flags

④ Design judgment

  • Narrowing against the published spec on main: install-local's accept set on main admitted a package whose declared datasource name was already held and silently dropped the declaration (200, hotLoaded: true, objects 404). With the datasources now registered as code and connected, admitting such a package would serve its objects from the holder's pool (the connect's idempotency guard) and make the holder's own datasource read-only to the admin door — the refusal makes the spec's own invariant (one holder per datasource name; code names are read-only) real at the consumer rather than taking a capability away. 409 RESOURCE_CONFLICT with details.datasource matches the door's existing refusal shape (namespace/base conflict). Holds.
  • The shared helper: one path for boot and install with three explicit installed deltas (names contributed now, fail-fast reported not thrown, withdraw-before-register + uninstall cleanup) is the right altitude; the boot path keeps its exact behaviour and the install path reuses the connection service's retained verdicts rather than inventing a status. The owned thunk preserves AppPlugin's "ownership resolution inside the lenient catch" invariant. Holds.
  • Public surface: the 4 exported types are the helper's parameter/return types, needed for a typed cross-package call; nothing else is exported. The datasources member is the minimal answer a CLI/agent needs to see a failed credential at install time without a second call. Holds.
  • Clients (Q7): ui/.../marketplaceApi.ts types the managed answer as an open TS interface (an added optional member is additive); CLI os package install handles unknown non-ok statuses with the generic printError of the body's error.message (the 409 reads as a named refusal); no spec Zod schema for the install-local answer exists to strict-parse it. Nothing breaks.

Implemented-by: claude/project-thread-aercb9
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Seat ACCEPT · PR #28132 · head dbc6f440b9aaf1b38d1d6c39442dbae91fd73683 · 2026-10-30T14:43Z

维护者速读

  • 改了什么: 通过 os package install(POST /api/v1/marketplace/install-local)装进运行中实例的包,现在会像启动时一样注册、记录并连接它声明的数据源(marketplace install-local: a package's declared datasources are never connected or registered as code-defined, so objects bound to them have no driver after install #28039)。以前只注册了对象,绑定到包自带数据源的对象读写一律 404。安装回答新增 datasources,列出每个数据源的连接结论。
  • 为什么改: 装好的包用不了自己的数据源,而同一个包作为启动工件加载时却可以;两条路现在共用一个运行时函数 connectPackageDatasources。
  • 风险与回滚: 新增一项拒绝:包声明的数据源名若已被占用(主机的 default、在 Setup 里建的、别的包的),安装返回 409 RESOURCE_CONFLICT,且在写任何东西之前拒绝;在 main 上这种安装是 200 成功(卡片线程实测),所以这是收窄,changeset 带 BREAKING。凭据解析不了时不拒装,而是该数据源显示失败、读取答 503。卸载会关连接池并撤回注册和名字。启动路径行为不变。回滚即还原 runtime 与 cloud-connection 两包的改动。
  • 先看: packages/runtime/src/declared-datasources.ts,和 marketplace-install-local-plugin.ts 的 2a 同名检查。
  • 两分钟验证: 跑 install-local-declared-datasources.dogfood.test.ts(8 例)和 declared-datasources.test.ts。
  • 需要你做的: 无(Tier S;契约复核 PASS 6524822720,按本期落地授权由席位落地)。凭据从哪来仍由 datasource: the credential refusals send a code author (defineDatasource) to a Setup form that cannot edit a code-defined datasource; name a binding path a code author can follow #27687 决定,本 PR 未作裁定。

席位核对

ACCEPT

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