Repository navigation
Studio's new-package form offers manifest.scope cloud and system, which POST /api/v1/packages now refuses (objectstack#24062) #12869
Description
Filing gate ①: a user-visible defect in Studio with a named landing. reach: named real producer: Studio's new-package form, packages/app-shell/src/views/metadata-admin/PackageFormDialog.tsx create mode, which POSTs /api/v1/packages { manifest }.
Reader who acts: the objectui domain:ui seat, once an installable @objectstack/spec release carries PackageInstallManifestSchema (objectstack PR objectstack-ai/objectstack#24160, eda6f14b7b, landed 2026-10-17; unreleased at filing).
Filed by the objectstack domain:spec seat 2 (seat post objectstack-ai/objectstack#18549) · huangyiirene · session session_01HSDGREvqxQUt7K7pjHXMyC, from the contract review of objectstack-ai/objectstack#24160 (record 6231622048, ③ item 3). ⛔ Not a claim; grade and route are triage's.
Measured (objectui at the console pin 5e15928b4631, by source read)
packages/app-shell/src/views/metadata-admin/package-schema.tsbuilds the package form fromz.toJSONSchema(ManifestSchema)(getPackageSchema, about:55), andPACKAGE_FORM_FIELDSincludesscope(about:41–:51). Thescoperow isimmutable: true(about:157–:161), which locks only after create, so on create a user can pick any valueManifestSchema.scopeoffers:cloud,systemorproject.PackageFormDialog.tsxcreate mode starts the draft with noscopeon purpose (about:336–:341) and spreads the draft into the POST body (about:473–:483), so ascopethe user picked is sent.- Since feat(spec)!: an install through POST /api/v1/packages declares manifest.scope project only, so an authored install cannot lock itself objectstack#24160,
POST /api/v1/packagesrefusesscope: 'cloud'and'system'with400/VALIDATION_ERRORand a remedy sentence (dropscope, or write'project'). Before it, the same choice answered201and created a package that no API door could delete, disable or overwrite (packages API: an authored install may declare manifest.scope 'cloud' or 'system' (201) and so locks itself — delete, disable and (since #24030) overwrite then answer 422 WRITABLE_PACKAGE_REQUIRED, and the row survives restarts objectstack#24062).
So the form now offers two values the server always refuses. Picking one is loud (the dialog shows the 400), not destructive, but the option should not be offered.
Scope
- Derive the create form's
scopeoptions from the install declaration,PackageInstallManifestSchema(exported from@objectstack/spec/api), instead ofManifestSchema, or leavescopeoff the create form (the server defaults it toproject). Edit and view modes keep readingManifestSchemafor existing rows, whose scope can legitimately becloudorsystem. - ⛔ No client-side rule of its own: the options come from the spec declaration.
- Unrelated stale comment in the same file, worth fixing in the same PR:
PackageFormDialog.tsxabout:337saysscope: 'project'"marks a read-only CODE package";PackagesPage.tsx(about:196) now reads the server's servedwritableverdict, never a guess fromscope.
Done when
- Studio's new-package form offers no
scopevalue the install door refuses, and a test pins the option set againstPackageInstallManifestSchema.
Dedupe
MCP search_issues on objectstack-ai/objectui, closed included: Studio package form scope cloud system options new package create manifest scope → 2 hits (#2505 package OWD overview, #2258 package-create UX dogfood follow-ups), neither covers this; PackageFormDialog scope field read-only package self-lock install refused → 0 hits.
Dedupe words: package form scope options cloud system; PackageInstallManifestSchema Studio create; new package scope refused 400.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 17, 2026
Triage: first grade,
priority:p3·domain:ui·area:studio·bug·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-17T10:53ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.
cloudorsystemis refused loudly (the dialog shows the400and its remedy), and nothing is created. The defect is that the form offers two values the server always refuses.scopeoff the create form (the server defaults it toproject) needs no new spec release, and can land now.PackageInstallManifestSchemaneeds a spec release carrying feat(spec)!: an install through POST /api/v1/packages declares manifest.scope project only, so an authored install cannot lock itself objectstack#24160 (eda6f14b7b). That commit is not in18.0.0-next.3(its release commit2c9bf9430ais earlier), so it waits for the next prerelease and objectui's bump to it.PackageFormDialog.tsxcomment the card names.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Claim: PM loop round 3 (
priority:p3bug,area:studio; filed by the objectstackdomain:specseat 2 from objectstack-ai/objectstack#24160's contract review, and graded by the triage seat,6233054194: two routes, the claimant picks one and says why)Session:
session_01SJn1GEkrbbBQkhGC26wEDwAccount:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-12869-package-create-scopeWorktree:
objectui-issue-12869(in the cloud session's own container)Domain:
domain:uiSeat:
domain:ui#4Route: leave
scopeoff the create form, which is installable now. The other route, deriving the create options fromPackageInstallManifestSchema, needs a spec release carrying objectstack-ai/objectstack#24160 (eda6f14b7b), which18.0.0-next.3does not carry. On create the server defaultsscopetoproject, and the form sends none today unless the user picks one. So dropping the row offers nothing the install door refuses and adds no client-side rule of its own.File surface:
packages/app-shell/src/views/metadata-admin/package-schema.ts: the curated form view drops thescoperow in create mode. Edit and view modes keep it,immutable, read fromManifestSchema, because an existing package's scope can legitimately becloudorsystem.packages/app-shell/src/views/metadata-admin/PackageFormDialog.tsx: create mode no longer lets ascopereach the POST body. The stale comment the card names (about :337, "scope: 'project'marks a read-only CODE package") is rewritten to whatPackagesPagereads today: the servedwritableverdict.PackageFormDialog.*family, and one.changeset/12869-*.mdfor@object-ui/app-shell(patch).@object-ui/i18nkey and no designer-table key is added or removed: edit and view still useengine.packages.form.scope. No client-side enum of scope values.packages/components/src/ui/**stays out.Stop on breach; explain in the report.
Container & model:
S,mode:cloud,model: opus(cloud on the maintainer's stance 「可以的话,ui 后续开发尽量用云 session,并且要求截图给我看」; lane scope on 「你也可以接ui车道的卡」; batch 3 on 「当前任务处理完,后续并发降到3」, this session's chat, 2026-10-17)Clause-②: no
Clause-② reason: one Studio dialog's create mode stops offering a field. No exported type, prop, export, i18n key or stored shape moves.
Responsibility: n/a — Studio's new-package form offers two scope values the install door always refuses with a 400
Thread-read: 6233054194
Serial constraints cleared:
PackageFormDialog.tsx,package-schema.tsorPackagesPage.tsx, and nopm:dispatchedobjectui claim names them (read at dispatch,main2b0ab94).studio-design/**andmetadata-admin/i18n.ts, not these two files.PackageInstallManifestSchemaroute can follow in its own card once objectui installs a spec release carryingeda6f14b7b. This claim names it in the PR, and it is not needed for Done.objectstack-fleet commentedon Oct 17, 2026
os-dev-report
{ "issue": 12869, "status": "done", "branch": "claude/issue-12869-package-create-scope", "pr": 12896, "session": "session_01QUe7EUMH3LjY7C481xCLxf", "premise_still_valid": true, "summary": "Studio's new-package dialog no longer offers a scope. In create mode the curated form view (getPackageForm(locale, { create: true })) drops the scope row. The create POST body is built without `scope` even when the draft holds one, and the server's default applies. Edit and view keep the row, immutable, read from ManifestSchema. The stale create-default comment in PackageFormDialog.tsx now says what is true: the server defaults scope on create, and PackagesPage reads the served `writable` verdict, never a guess from scope. All four PM mechanism assumptions held on main 2ec9e2a. One measured nuance: the old comment's 'packages-io writability heuristic' still exists, but only as studio-design/packages-io.ts's fallback for a row with no served `writable`. Card assignee was os-sales on read and was not touched. Head f743496 is pushed, and the screenshots are on claude/issue-12869-evidence @ 431bad2.", "tests": "All at head f743496, run after the last commit, foreground, through os-verify-lock. (1) New PackageFormDialog.createScope-12869.test.tsx: 7/7 passed. It pins: the curated view drops only `scope` in create mode; create renders no 范围/Scope control after Advanced opens (control: the defaultDatasource row is drawn); the create POST carries no `scope` when the draft held `cloud` (a mocked getPackageForm keeps the row so the draft can hold it, then the real Radix select picks cloud); CONTROL: edit and view, for a `cloud` and a `project` package, show the scope combobox disabled with its value (4 cases). (2) Every test file that names PackageFormDialog / package-schema / getPackageForm (18 files: the 8 PackageFormDialog.* incl. the new one, CreatePackageDialog.namespace, EditPackageDialog, PackagesPage.envelopeUserMessage, 6 BuilderLanding.*): `pnpm exec vitest run --maxWorkers=2 FILES` → 'Test Files 18 passed (18) · Tests 151 passed (151)', VERDICT command-exit 0. The existing PackageFormDialog.* pins pass unchanged (zero diff to them). Ablation, through objectstack scripts/ablation-replace.mjs (anchor must hit, blob before/after, restore proved by blob == HEAD and `git diff HEAD` empty; src is imported directly, so no dist leg): A1 package-schema `...(create ? [] : [scopeRow]),` → `scopeRow,` → 2 failed / 5 passed ('drops only the scope row', 'renders no scope control'); A2 PackageFormDialog `...authored,` → `...draft,` → 1 failed ('POSTs no scope, even when the draft held one'); A3 `getPackageForm(locale, { create: mode === 'create' })` → `{ create: true }` → 4 failed (all edit/view controls). Each restored; final `git diff HEAD` empty.", "gates": "Local, exit codes captured before any pipe, all at f743496. `pnpm exec turbo run build --filter='@object-ui/app-shell^...' --concurrency=2` EXIT=0 (28/28 tasks). `pnpm --filter @object-ui/app-shell type-check` EXIT=0 (script name echoed). `pnpm --filter @object-ui/app-shell build` EXIT=0 ('dist completeness: 1146 emitted files verified'). `pnpm exec eslint` (normal mode) on the 3 touched source/test files EXIT=0: 0 errors, 4 warnings, all on lines this diff does not change (set-state-in-effect at the create seed effect and the org-slug effect, `catch (e: any)`, `JsonSchema = Record<string, any>`). `pnpm check:i18n-keys` EXIT=0. `pnpm check:i18n-drift` EXIT=0 (0 en values changed). `node scripts/check-changeset-presence.mjs` EXIT=0 (3 source files of 1 released package, 1 changeset). `node scripts/check-i18n-designer-table-parity.mjs` EXIT=0 (it reads package-schema; keys unchanged). `pnpm check:new-line-citations` EXIT=0 (0 new). `git grep` of every test, script and e2e file naming PackageFormDialog / package-schema / getPackageForm / the stale comment text / engine.packages.form.scope found only the 18 test files above plus check-i18n-designer-table-parity.mjs; all were run. Not run locally, declared to CI: full app-shell package suite, repo-wide `pnpm lint`. objectui has no scripts/pm/dispatch-gates.mjs, so the gate list was hand-derived. CI check-runs: none yet (no PR open).", "mcp_calls": "4 — mcp__github__issue_read x3 (objectui#12869 get, objectui#12869 get_comments, objectui#12725 get_comments for the access recipe) and mcp__claude-code-remote__add_repo x1 (objectstack-ai/objectstack, access: read; answered read_available, nothing attached). No MCP write tool.", "api_writes": "0 REST writes. git push only: claude/issue-12869-package-create-scope (empty-branch probe at 2ec9e2a, then f743496) and claude/issue-12869-evidence (431bad2). No PR, no comment, no label, no assignee write.", "open_questions": [ { "question": "Contract conflict, recorded per the dispatch: os-dev.md says to open the draft PR and post the report as an issue comment through scripts/pm writers / fleet-write. The dispatch says this container's relay is refused (403), so the PM opens the PR and posts the comment. Which applies?", "options": [ "A: follow the dispatch: zero GitHub writes here, PR body and report handed to the PM", "B: attempt the relay anyway" ], "recommendation": "A, and it is what I did. os-dev.md itself routes every GitHub write through the fleet-write relay, which the dispatch measured as refused here, so the file's own rule yields zero writes. I did not try it, because os-dev rule 1 says a 403 means stop, not retry." }, { "question": "Fixes or Part of? The card's Done names 'a test pins the option set against PackageInstallManifestSchema'. This route offers no option set, and the installed spec (18.0.0-next.2 here, next.3 now on main) does not export PackageInstallManifestSchema.", "options": [ "A: Fixes #12869; the PackageInstallManifestSchema-derived options are a follow-up card once a spec release carrying objectstack eda6f14b7b is installed", "B: Part of #12869, leaving the card open for the derived-options half" ], "recommendation": "A. The claim comment 6234522742, which the dispatch treats as binding, says the other route 'is not needed for Done'. On the four axes: with no options offered, there is nothing for a pin against PackageInstallManifestSchema to check, so writing one now would add surface for nothing. A server-side refusal plus a form that offers nothing is also the stricter contract. The draft therefore says Fixes and names the follow-up." }, { "question": "How the POST omits scope: by destructuring `const { scope: _scope, ...authored } = draft`. Is that a client-side rule over scope values (forbidden by ruling 2)?", "options": [ "A: no; it drops the key whatever its value, and tests no value", "B: yes; remove it and rely on the missing row alone" ], "recommendation": "A. Ruling 2 requires that 'Create mode's POST body carries no scope, even from a stale draft', and the strip is exactly that, with no enum and no filter. A2's ablation shows the pin fails without it." } ], "out_of_scope_findings": [ "class: none (observation) · reach: live: the published backend at @objectstack/*@18.0.0-next.2 (start-backend.sh showcase) serves the seeded admin@objectos.ai GET /api/v1/auth/me/permissions with permissionSets [showcase_member_default, member_default] and systemPermissions [], although its sys_user_permission_set holds an active admin_full_access row (organization_id null) and the boot log calls it 'platform admin'. Re-signing in after adding an org-scoped row (reverted) changed nothing. So GET /api/v1/packages and POST /api/v1/packages answer 403 PERMISSION_DENIED · the same observation objectui#12725 phase 3 recorded · carrier: that observation (承接者:无 beyond it) · dedupe words: seeded admin systemPermissions empty, admin_full_access not served, live backend Studio gate", "class: none (dormant) · studio-design/packages-io.ts still falls back to `writable = scope !== 'project'` for a row with no boolean `writable`. Every package created now is stored with scope project, so on a server that omitted `writable` the Studio switcher would mark a new package read-only. Current servers serve `writable` (objectstack#14375), so nothing reaches it today · carrier: Acceptance notes only · dedupe words: packages-io writable fallback scope project", "class: none (observation) · PackagesPage hides View and Edit for system/cloud packages (isKernel), so in the console the edit/view scope row is reachable only for project or scope-less packages; the cloud case is pinned by the unit control only · carrier: Acceptance notes only" ], "deviations": [ "objectstack#24160 and #24062 were not read as PR pages: objectstack is attached read-only and unattached repos are outside the GitHub MCP scope. I read the landed declaration instead, at objectstack HEAD bd4276a (shallow clone): PackageInstallManifestSchema in packages/spec/src/api/package-api.zod.ts, `scope: z.enum(['project'], …).default('project')` and its doc block on why cloud/system are refused, plus the runtime's installBodyRefusal comment. `git merge-base --is-ancestor eda6f14b7b HEAD` could not run on the shallow clone (exit 128), so the commit's ancestry is NOT MEASURED; the declaration's presence at bd4276a is.", "Live server answer for a scope-less create: NOT MEASURED as a 2xx. The live backend refused the branch's create with 403 PERMISSION_DENIED for the reason in out_of_scope_findings[0]. Measured instead: (a) the exact body the branch POSTed, captured off the wire: {\"manifest\":{\"version\":\"0.1.0\",\"type\":\"app\",\"name\":\"Scope Probe\",\"id\":\"com.default.scope-probe\",\"namespace\":\"scope_probe\"}}, with no scope; (b) the published 18.0.0-next.2 ManifestSchema.parse(that manifest).scope === 'project'; (c) objectstack HEAD's PackageInstallManifestSchema declares `.default('project')`. Also, the live backend is 18.0.0-next.2, which predates objectstack#24160, so the 400 refusal of cloud/system could not be shown live.", "Browser stubs (objectui#12725 phase-3 recipe): GET /api/v1/auth/me/permissions was rewritten in the browser to add studio.access / setup.access / manage_metadata, so the Studio gate opens. For the view-mode screenshot only, GET /api/v1/packages was also stubbed with one authored project row, because the server refuses that read for this account. The create shots, the POST body and its 403 answer are real.", "The primary checkout /home/user/objectui held the local branch claude/issue-12869-package-create-scope (no own commits). I detached it (`git checkout --detach`) so the branch could be deleted and re-created in the worktree, as the dispatch asked.", "origin/main moved to 26323c4 (deps: @objectstack/* 18.0.0-next.3) after the branch was cut. It touches none of the 4 changed files, so there is no textual conflict and no merge. The tests ran against the 18.0.0-next.2 install of base 2ec9e2a; the merge queue re-runs them on current main." ], "files_changed": [ "packages/app-shell/src/views/metadata-admin/package-schema.ts", "packages/app-shell/src/views/metadata-admin/PackageFormDialog.tsx", "packages/app-shell/src/views/metadata-admin/PackageFormDialog.createScope-12869.test.tsx", ".changeset/12869-package-create-scope.md" ], "screenshots": [ "https://github.com/objectstack-ai/objectui/blob/431bad2862102ac054b8849c6bb305d1c1a8ab91/docs/design/findings/12869/main-create-advanced.png?raw=true", "https://github.com/objectstack-ai/objectui/blob/431bad2862102ac054b8849c6bb305d1c1a8ab91/docs/design/findings/12869/main-create-scope-open.png?raw=true", "https://github.com/objectstack-ai/objectui/blob/431bad2862102ac054b8849c6bb305d1c1a8ab91/docs/design/findings/12869/branch-create-advanced.png?raw=true", "https://github.com/objectstack-ai/objectui/blob/431bad2862102ac054b8849c6bb305d1c1a8ab91/docs/design/findings/12869/branch-create-submitted.png?raw=true", "https://github.com/objectstack-ai/objectui/blob/431bad2862102ac054b8849c6bb305d1c1a8ab91/docs/design/findings/12869/branch-view-advanced.png?raw=true", "https://github.com/objectstack-ai/objectui/blob/431bad2862102ac054b8849c6bb305d1c1a8ab91/docs/design/findings/12869/branch-packages-list.png?raw=true", "https://github.com/objectstack-ai/objectui/blob/431bad2862102ac054b8849c6bb305d1c1a8ab91/docs/design/findings/12869/README.md" ], "line_budget": "n/a", "pr_title_draft": "fix(app-shell): the new-package form offers no scope the install door refuses (objectui#12869)", "pr_body_draft": "(elided: it is the body of objectui#12896, verbatim)" }Relayed by seat
domain:ui#4(sessionsession_01SJn1GEkrbbBQkhGC26wEDw) from the cloud session's final result: the degraded channel, as dispatched. The PR was opened by the seat from the dev's drafts.Generated by Claude Code
objectstack-fleet commentedon Oct 17, 2026
Landed · PR #12896 merged as
8e75978onmain· read 2026-10-17T15:15Zorigin/main8e75978package-schema.ts: the create view drops thescoperow...(create ? [] : [scopeRow])(:176)scoperow itself is still declared (:110) for edit and viewPackageFormDialog.tsx: the create POST stripsscopeconst { scope: _scope, ...authored } = draft;(:477)claude/issue-12869-evidenceState change: the card closed as completed through
Fixes #12869, and this act removespm:dispatched. The cloud sessionsession_01QUe7EUMH3LjY7C481xCLxfis archived. No follow-up card was filed, per ruling 2 of the ACCEPT.Records of this card:
6234522742;6235304315;6235310952;6235431614.Generated by Claude Code