Skip to content

fix(metadata-protocol,runtime,app-shell)!: refuse a save or draft publish naming a package that is not installed - #26920

Merged
objectstack-fleet[bot] merged 17 commits into
mainfrom
claude/project-thread-oviakg
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 17 commits into
mainfrom
claude/project-thread-oviakg

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #26708

Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: updated here — content/docs/concepts/metadata-lifecycle.mdx (no skills/** page teaches saving into a named package: skills/objectstack-api names ?package= only as a read parameter)

维护者速读

  • 改了什么:/meta 保存时若 ?package= 指向本部署未安装的包,所有类型(不只 flow)的草稿和直接保存都会被拒(422 WRITABLE_PACKAGE_REQUIRED,什么都不写);publish-drafts 对未知包 id 先回 404;Studio 打开不存在的包显示"未找到包",未知栏目显示"页面不存在"。
  • 为什么改:之前拼错包 id 也能保存成功,生成挂在"幽灵包"下的对象/仪表盘/页面,没有命名空间、无法卸载或导出,publish-drafts 还会把它们发布上线。
  • 风险与回滚:只收紧请求,不改存储形状;已存在的幽灵行照常可读,可用 discard-drafts 清理。回滚即还原本 PR,无数据迁移。夹具改动量大(约 50 个测试文件改为先安装包),是规则收紧的直接后果。
  • 先看:packages/metadata-protocol/src/protocol.ts(uninstalledNamedBaseRefusal 与 saveMetaItem 的调用点)、packages/runtime/src/domains/packages.ts(publish-drafts 的 404 位置)、ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx(pkgMissing 判定)。
  • 两分钟验证:pnpm dev -- --fresh -p 4417 后登录,PUT /api/v1/meta/dashboard/x?mode=draft&package=com.example.nosuch 应回 422 WRITABLE_PACKAGE_REQUIRED;再打开 /_console/studio/com.example.nosuch/data 应看到"未找到包"。
  • 需要你做的:无,席位落地。

What changes

  • @objectstack/metadata-protocol — the flow-only named-base rule (metadata: the /meta flow save accepts a write bound to a package id that no installed package has, and stores it active #20863) is now one private helper, uninstalledNamedBaseRefusal, and saveMetaItem asks it for every type, draft and active, on authoring saves (not on the migrate-stored and package-duplicate rewrites of stored rows). A save naming a package id the registry does not hold is refused 422 WRITABLE_PACKAGE_REQUIRED with packageId and the ADR-0070 docs link, before anything is written or registered. The flow arm (tenantAuthoredWriteRefusal) calls the same helper, so its sentence is unchanged byte for byte and other types get the same sentence with their type name. The sys_metadata sentinel, a save naming no package and a save naming an installed package are unchanged.
  • @objectstack/runtime — POST /packages/:id/publish-drafts on an id the registry does not hold answers 404 RESOURCE_NOT_FOUND (the GET /packages/:id answer, the existing unknown-id code) before the protocol is asked, even when drafts are stored under that id. The check sits after the capability probe, like discard-drafts and the commit trail, so a host that cannot publish still answers 501. No error code was added. Stored rows under such an id are not migrated or deleted.
  • @object-ui/app-shell — /studio/ID/… for an id the package list does not hold renders "Package not found" (no pillars, no Changes / Publish / create-app, one button back to the Studio landing). A pillar segment Studio has no pillar for renders a not-found state instead of falling through to Interfaces. It reads the existing GET /api/v1/packages (no new endpoint, still one request): a switcher entry counts as installed; otherwise the id is looked up among every listed id, so kernel packages the switcher hides still open. A list that failed to load leaves the builder as it was.
  • Docs: one paragraph in content/docs/concepts/metadata-lifecycle.mdx; changeset 26708-named-base-every-type.md (patch on the three packages, BREAKING banner, both declaration lines).

Why

PUT /api/v1/meta/:type/:name?package=ID stored the item bound to ID even when no installed package had that id, as a draft or live, and with no manifest the object namespace rule was skipped too. publish-drafts on that id then put the drafts live, owned by a package that cannot be uninstalled or exported. Only flow refused it.

Tests

  • New packages/metadata-protocol/src/protocol.named-base-every-type.test.ts (13 tests): object, dashboard and page, draft and active, refused with the envelope (code, status, packageId, message first sentence) and nothing inserted or registered; the flow arm's exact sentence; a plural type spelling; controls for an installed package, the sentinel / null / no package, and the two server-side rewrites.

  • packages/runtime/src/domains/packages-draft-doors-unknown-package.test.ts: drafts stored under an unknown id are not published (404, the protocol is not asked, no visibility flip); controls that a known package still publishes and an unknown id's drafts are still discarded.

  • New ui/…/StudioDesignSurface.notFound-26708.test.tsx (10 tests): not-found on all four pillars with no pillar links or package actions and one list request, the way back, controls for a listed package, a kernel package and a failed list, and the unknown pillar segment.

  • Red first: on the base tree the protocol pins fail 7 of 13 (the 6 controls pass), the runtime pin fails, and the Studio pins fail 6 of 10 (the 4 controls pass).

  • Fixtures: about 50 test files across metadata-protocol, objectql, runtime and rest saved into package ids that were never installed. Each now installs the package it names, as a boot or POST /packages does (authored, or code where the file's premise is a shipped package). Where the package's closure now judges a view's object (list-view-object-outside-closure), the fixture registers the bound object under the package or declares the owner as a dependency. None of these widened the rule.

  • Dogfood: three files saved into package ids no boot installs (package-first-authoring, package-uninstall-revokes-named-grants, flow-shipped-name-published-door). Each now creates its base package first through protocol.installPackage (as POST /api/v1/packages does) under a valid reverse-domain id with its own namespace; the old ids used underscores, which the install door refuses as invalid package ids.

  • StudioDesignSurface.flowCanvasChip-11665.test.tsx: the zh tooltip assertion now waits for the lazily fetched studio catalogue it reads (it answers en until the zh catalogue lands and re-renders the pillar). It passed before only because of module load order; adding two keys to i18n.ts changed that order.

  • Package suites on the final head (48af4d05fc, after merging main at 96e7a4335c, which holds fix(runtime)!: the package draft doors refuse a query parameter they do not read #26853's draft-door query refusal in the same handler): @objectstack/runtime 7468 passed / 19 skipped, @objectstack/metadata-protocol 21679 passed / 26 skipped, @objectstack/objectql 8843 passed, @objectstack/rest 6513 passed / 326 skipped; typecheck exit 0 on all four. UI packages/app-shell/src/views/studio-design + metadata-admin: 835 files, 8321 passed / 1 skipped; @object-ui/app-shell type-check exit 0.

  • scripts/engine-double-contract.pinned.json: the new protocol test's engine double answers delete, findOne and update it never reads; recorded with pnpm check:engine-double-contract --write.

Acceptance notes

  • How rows already stored under an uninstalled id surface (live, on one dev DB written by main, then booted on this branch): the active row still reads (GET /meta/dashboard/NAME 200) and still lists under ?package=ID; its draft still reads; a save naming the id is refused 422; publish-drafts answers 404; discard-drafts answers 200 and removes the drafts; the id is not in GET /api/v1/packages, so Studio shows "Package not found" for it.
  • Two doors still write or publish under such an id, both outside this card's ruling: a save naming NO package for an item whose active row is bound to the phantom id inherits that binding (SqlDriver org auto-scope makes SysMetadataRepository.listDrafts blind to env-wide drafts (organization_id IS NULL ∧ injected org = always empty); mode=draft saves drop package_id #11087), and the per-item POST /meta/:type/:name/publish?package=ID publishes a phantom-bound draft (200). Both only reach rows written before this change.
  • Studio's recent-packages list can still hold an id that later reads "Package not found"; the not-found page offers the way back.

Verification

  • Live red/green on pnpm dev -- --fresh, signed in as the dev admin, same probe on both trees. Base (4f05de4): six PUT /api/v1/meta/{object,dashboard,page}/NAME[?mode=draft]&package=com.example.nosuch all answered 200 and stored the rows bound to that id; POST /packages/com.example.nosuch/publish-drafts answered 200 while GET /packages/com.example.nosuch answered 404. This branch: the same six saves answer 422 WRITABLE_PACKAGE_REQUIRED ("Cannot save dashboard 'ghost_dashboard_a' into package 'com.example.nosuch': no package with that id is installed in this deployment, …"), publish-drafts answers 404 RESOURCE_NOT_FOUND. Controls unchanged on both: a save naming no package (200), and a package created through POST /api/v1/packages then saved into and published (201, 200, 200).

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on 48af4d05fc: 105 commands, each run with its exit code captured before any pipe; all 105 exit 0. --ran reconciliation: "105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN".

  • Full dogfood once on the final head 48af4d05fc (pnpm --filter @objectstack/dogfood test): exit 0, 327 files passed / 3 skipped, 2683 tests passed / 20 skipped.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec untouched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (Scoped to the touched files on the final head: root ESLint exit 0; ui ESLint exit 0, 0 errors, 14 warnings, all on lines this PR did not write — the one inside the edited effect is the existing setPkgWritable(undefined) call.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5nhhuhjVQJ8R3qK9UB

claude added 14 commits October 27, 2026 15:57
…lled on every /meta save and on publish-drafts

The flow-only named-base rule is lifted into one helper that saveMetaItem
asks for every type, draft and active; publish-drafts refuses an id the
registry does not hold before anything is published.

Claude-Session: https://claude.ai/code/session_01FEkQ5nhhuhjVQJ8R3qK9UB
Co-authored-by: Claude <noreply@anthropic.com>
…e saves bind

A save naming an installed package is judged against that package's closure,
so each fixture that now installs its package also lists the objects its
views and flows bind.

Claude-Session: https://claude.ai/code/session_01FEkQ5nhhuhjVQJ8R3qK9UB
Co-authored-by: Claude <noreply@anthropic.com>
A save naming a package no installed package has is now refused for every
type. Each fixture installs the authored or code package its saves name, as a
boot does, and registers the object a grid binds where the package's closure
now judges it.

Claude-Session: https://claude.ai/code/session_01FEkQ5nhhuhjVQJ8R3qK9UB
Co-authored-by: Claude <noreply@anthropic.com>
…pability probe; fixtures install the packages they name

A host whose protocol cannot publish answers 501 whatever id it is asked about,
as discard-drafts and the commit trail do. The runtime and rest fixtures that
save into a package now install it first, as a boot or POST /packages does.

Claude-Session: https://claude.ai/code/session_01FEkQ5nhhuhjVQJ8R3qK9UB
Co-authored-by: Claude <noreply@anthropic.com>
… no entry for the package

A switcher entry is an installed package, so the not-found check reads the
full id list only for an id the switcher lacks; both reads still share the
one package-list request.

Claude-Session: https://claude.ai/code/session_01FEkQ5nhhuhjVQJ8R3qK9UB
Co-authored-by: Claude <noreply@anthropic.com>
…t reads

`designer.field.apiName` comes from the lazily fetched studio namespace, which
answers from en until the zh catalogue lands and re-renders the pillar. The
assertion read the title before that landing and only passed on load order.

Claude-Session: https://claude.ai/code/session_01FEkQ5nhhuhjVQJ8R3qK9UB
Co-authored-by: Claude <noreply@anthropic.com>
Four dogfood files saved into package ids no package had, three of them ids
the install door refuses outright (underscores are not a reverse-domain
segment). Each now installs its base through the protocol's install primitive,
under a valid id with the namespace its object names carry, and the uninstall
case installs the package again before it republishes.

Claude-Session: https://claude.ai/code/session_01FEkQ5nhhuhjVQJ8R3qK9UB
Co-authored-by: Claude <noreply@anthropic.com>
Brings in #26919, which renames the package draft doors' query check to
refusePackageWriteDoorQuery and asks it on every package write door. On
publish-drafts the query check still runs first, then the unknown-id 404.

Conflicts: the Studio not-found strings re-added after desktopHint in
main's i18n.ts; the engine-double ledger regenerated with --write.

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

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/runtime, touching 11 documentable anchor(s).

18 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 5e99bd936c4ecd60aa82eb72709e5f7f0d810583.

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

What this run could not see
  • the SDK route bridge reached 55 of 207 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 5e99bd936c4ecd60aa82eb72709e5f7f0d810583 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 5e99bd936c4ecd60aa82eb72709e5f7f0d810583

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

… save into

The named-base rule now refuses a save naming a package the registry does
not hold, for every type. These suites saved views and permission sets into
package ids nothing installed. Each now installs the package first, as a
boot or POST /packages does: the client and cli door harnesses install both
packages (depending on the package that owns `task`), the kernel
integration tests create theirs through protocol.installPackage under a
valid hyphenated id, and plugin-security's real registry installs the
runtime package while its stub registry answers getPackage for it.

Claude-Session: https://claude.ai/code/session_01FEkQ5nhhuhjVQJ8R3qK9UB
Co-authored-by: Claude <noreply@anthropic.com>
Brings in #26942, which moves the Studio strings into the studio i18n
namespace. The two Studio not-found strings this branch adds move with
them: from app-shell's metadata-admin table into
@object-ui/i18n's studio namespace (en and zh), beside desktopHint.

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

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-28T01:25Z · head 380f37a55a · #26708

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
  2. Against the order's rulings:
  3. Red first and live (the dev's):
    • On the base tree, the protocol pins fail 7 of 13 (the 6 controls pass), the runtime pin fails, and the Studio pins fail 6 of 10 (the 4 controls pass).
    • Live on pnpm dev -- --fresh: six saves into com.example.nosuch answered 200 on base and answer 422 WRITABLE_PACKAGE_REQUIRED on the branch. publish-drafts went from 200 to 404. The controls are unchanged on both trees.
  4. Fixtures: about 50 test files saved into package ids that were never installed. The dev fixed each to install its package first; none widened the rule.
    • The first CI run on the merge head d1002180 was red in cli, client and plugin-security. All three were this PR's fixtures, reproduced locally and fixed the same way in 019fbe0: cli 49 of 49, client 38 of 38, plugin-security 25 of 25. The plugin-security red was not the Queue-flake anchor: src/delegated-organization-membership.test.ts #26828 flake.
    • 380f37a5 merges main c6853b124d. The one conflict was ui/packages/app-shell/src/views/metadata-admin/i18n.ts, where main moved Studio strings into @object-ui/i18n. The dev took main's file and added this PR's two keys, engine.studio.notFound.package and .pillar, to the studio namespace (en.ts, zh.ts) beside desktopHint; the tFormat call sites are unchanged. On that head: build 116 of 116, studio-design and metadata-admin 8,511 passed, both type-checks 0, check:i18n-designer-parity 0, and the 13 test files main added since the last merge that save or publish all green.
    • Two kernel tests now create their package through protocol.installPackage under a hyphenated id, because the install door refuses underscores.
  5. Tests and dogfood:
    • Merge head: runtime 7,555, metadata-protocol 21,706, objectql 8,854, rest 6,529, and ui 846 files, all green. Type-check is 0 on every touched package.
    • Dogfood ran in full on 48af4d05fc, the last behaviour change: 327 files and 2,683 tests passed. The later commits are a merge of main and fixture fixes.
  6. Gates:
    • Locally 112 of 112 derived commands exit 0, re-derived on 380f37a5 (the i18n families included), with 0 NOT-MEASURED and 0 UNRUN.
    • At this stamp every check-run on the head reads success or skipped: 57 success, 6 skipped, none failed or in progress. GitHub reads the PR clean.
  7. Overlap: git merge-tree reports no conflict against main a8ee8a229a.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 01:26
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 01:26
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 4f93696 Oct 28, 2026
64 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-oviakg branch October 28, 2026 02:09
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

2 participants