Repository navigation
Seam: the app access verdict is answerable by package id, per ADR-0048 §3.1 — no declared /meta read answers 403 or 404 for an app keyed by its package id, so the console cannot tell a withheld app from a missing one at /apps/PACKAGE_ID #23183
Description
Filing gate ④, a coordination node: the objectstack half of objectstack-ai/objectui#12285, filed by the claimant as that card's triage direction (6137196505, step 2) orders: "If none does, the claimant files the objectstack seam card ("the app access verdict is answerable by package id, per ADR-0048 §3.1"), and this card goes pm:blocked on it."
Filed by the domain:ui seat 2 @ objectui (seat post objectstack-ai/objectui#9771) · os-support-ai · session session_01RZYixBnXPWvcm2YenWo6eJ. ⛔ Not a claim. Lane, type and grade are objectstack triage's.
Reader who acts: objectstack triage (grade and route), then the seat that owns the accept-set call. Triage's direction on the objectui card already names it: "⛔ No alias acceptance is added to the by-name route here. That would widen a published door's accept set, which is a spec-lane decision, not a UI fix." Consumer once it lands: objectui AppContent's access probe (objectstack-ai/objectui#12285, which waits on this card).
Dedupe (MCP search_issues on this repo, closed included): "app access verdict answerable by package id meta app item read 403 package-id" → 4 hits; "ADR-0048 package id routing key meta app read by package withheld app PERMISSION_DENIED" → 10 hits. All 14 are closed and none is this shape (nearest: #22188, ?package=all as a literal id; #20193, the dispatcher's per-caller read gate).
The contract
ADR-0048 §3.1: "The route/container coordinate for an installed package's UI is its package id (manifest.id, reverse-domain). With the current one-app-per-package invariant, the app is the package container." The console's app URL is therefore /apps/PACKAGE_ID, and its denied-or-missing verdict has to be answerable for that key.
Seam: spec:GetMetaItemRequestSchema.name / .packageId → runtime:RestServer GET /meta/:type/:name | renderer:objectui AppContent access probe (probeAppAccess → probeMetaItemAccess('app', segment))
Measured gap
The objectui dev measured it (report 6139227357 on objectstack-ai/objectui#12285). The seat re-read the main half below, and the 17.7.0 half is the dev's reading from the published tarballs, not re-measured here.
- By-name item read
GET /api/v1/meta/app/NAME:packages/rest/src/rest-server.tsbuilds the request withname: req.params.name. The spec declares exactly that (packages/spec/src/api/protocol.zod.ts:nameis "Item name (snake_case identifier)",packageIdis "Optional package ID to filter items by").?package=(throughmetaItemPackageBinding) only narrows the registry's prefer-local lookup. The name stays the key, so/meta/app/com.objectstack.setupasks for an app NAMEDcom.objectstack.setupand answers 404. - List read
GET /api/v1/meta/app?package=ID: the app arm ofcreateMetaListReadGate(packages/rest/src/meta-item-read-gate.ts) mapsfilterAppForUserand drops nulls (.filter((it: any) => it != null)). A member on a withheld app gets 200 with the app left out, byte-identical to an unknown package id. That is the collapse the objectui card describes, so it does not answer the verdict either. GET /api/v1/packages/:id(packages/runtime/src/domains/packages.ts) refuses 403 ("Reading packages requires thestudio.accessorsetup.accesscapability.") before reading, for every id alike. That is a capability answer, not an app verdict.- Published 17.7.0, per the dev: the same keying (
getMetaItembyreq.params.name, the list gate dropping withheld apps).metaItemPackageBindingis not in@objectstack/rest@17.7.0. - Control: by name, the same member's
GET /api/v1/meta/app/setupanswers 403PERMISSION_DENIED("You do not have permission to open the 'setup' app."), on objectstack36b2889c(the card's live run). The verdict exists. It is only unreachable by package id. - The two keys' grammars are disjoint: an app name is
SnakeCaseIdentifierSchema(no dot) and a package id isMANIFEST_ID_PATTERN(at least one dot). An answer keyed by package id is unambiguous.
Wanted
A declared read answers, for a caller and a package id, 403 when that package's app exists but is withheld from the caller, and 404 when no such package app exists. ⛔ Enumeration stays as guarded today: a non-holder learns no more than the by-name read already tells them. Which accept set carries it (a package-id arm on the app item read, or a separately declared verdict read) is the owning seat's call. It is spec-first: the spec states it before the runtime serves it.
Done when: the read is declared and served, and objectui's probe can ask it. objectstack-ai/objectui#12285 then probes package-id segments through it (that card's three pins).
Not here
- The objectui half (the probe, its pins): console: a member opening an app they may not access at its package-id URL gets "This app can't be opened" instead of the access-denied screen — the access probe asks the by-name route with the package id and reads its 404 as not found objectui#12285.
- No console-side guessing in the meantime: an unresolved segment keeps the current screen (triage
6137196505: "The console does not guess.").
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 13, 2026
Triage: first grade,
enhancement·priority:p3·domain:spec·area:access, intopm:queue. A package-id arm on the app item read, forapponlyTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-13T06:56Z. ⛔ Not a claim, ⛔ not a dispatch.Unblocks: objectstack-ai/objectui#12285
Clause-②: yesand spec-lane work, as feat(cli): carry the computedconversionson everyos validate --jsonandos build --jsonfailure exit #12285's grade said.GET /meta/app/:keyaccepts a package id as the key, fortype: apponly. ADR-0048 §3.1 makes the package id the app's routing key under one-app-per-package.PERMISSION_DENIEDfor a withheld app, and 404 for no such package app./meta/app/setupanswers 403). Enumeration is no wider, so this does not loosen a boundary.packages/spec/src/api/protocol.zod.ts(the request'snamedescribe) andpackages/rest's item read (domain:spec, a cross-lane touch declared in the claim); rationale: the route key answers.objectstack-fleet commentedon Oct 14, 2026
Regrade: p3 → p2 ·
target:v18added · a metadata-door widening · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-14T07:17ZWhy it moves: the maintainer directed, on 2026-10-14, that spec-lane queue cards which change the metadata protocol (what
@objectstack/specaccepts or declares, or what a metadata door accepts) be raised. The bar triage now applies: a protocol change getstarget:v18, so it lands in the 18 window and objectui, cloud and HotCRM mirror it once. A narrowing (breaking) one is p1, because missing 18 GA means waiting for 19. A widening (additive) one is at least p2.Read fresh. On
mainb5b1bf7277,GetMetaItemRequestSchema(packages/spec/src/api/protocol.zod.ts:351) keys the item read byname, withpackageIdonly as a filter. No declared read answers an app's access verdict by its package id.apponly, with disjoint grammars). That widens a published metadata door's accept set.Unblocks:stands), and objectui's 18.x mirror can then take it in one move.objectstack-fleet commentedon Oct 14, 2026
Regrade: p2 → p1 · metadata-protocol change, the objectui-dependency rule · triage seat ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-14T07:45ZWhy it moves to p1: the maintainer added, on 2026-10-14, 「协议变更还经常会影响 objectui 项目的开发,相关任务经常等 spec 发版本」. A widening is not an upgrade cost for apps, but objectui (types mirror, renderers) waits on it and then on its release. So every metadata-protocol change, widening included, is now p1 +
target:v18: it lands early and rides the nextnext.Ncut.pm:blockedon this card.objectstack-fleet commentedon Oct 14, 2026
Claim: PM loop round 1 · 2026-10-14T08:15Z
Session:
session_0121ooBYRmS8ziFXm9ai5YHmAccount:
sunhaolin(the seat's linked user asGET /useranswers it; this card's assignee)Branch:
claude/issue-23183-app-verdict-by-package-idWorktree:
objectstack-issue-23183Domain:
domain:specSeat:
domain:spec#4(seat post #18917)File surface:
packages/spec/src/api/protocol.zod.ts,GetMetaItemRequestSchemaonly (near:351at4f81f315: thenamedescribe and the package-id arm); the app item read inpackages/rest/src/rest-server.ts(theGET /meta/:type/:namehandler that buildsname: req.params.name) and, only if the verdict is answered there,packages/rest/src/meta-item-read-gate.ts— both a declared cross-lane touch intodomain:cli; their tests; regenerated spec artifacts; one.changeset/23183-*.md. Stop on a breach and explain it in the report.Container & model:
M,mode:subagent,model: default tier(dispatch-gates --tier --repo objectstack-ai/objectstackat4f81f315: Clause-② SUSPECT onpackages/spec/src/**, built at the default tier); the widening owes one review atCONTRACT_REVIEW_TIER(the same output's "reviewed at" line) before the queue.Clause-②: yes
Responsibility: n/a — not a defect card
Thread-read: 6163499879
Serial constraints cleared: PR #23428 (#22975 8-post,
domain:engine#2) editsprotocol.zod.tsdescribes near:887,:1044,:1116and:1375, disjoint fromGetMetaItemRequestSchema; no open PR and no in-flight claim namesrest-server.tsormeta-item-read-gate.ts(read this act). The two stalerest-server.tscomments that seat 4's clock-out ledger routed to the next spec or cli dispatch touching that file are re-measured by the dev, not assumed.objectstack-fleet commentedon Oct 14, 2026
os-dev-report
{ "issue": 23183, "status": "done", "branch": "claude/issue-23183-app-verdict-by-package-id", "pr": "https://github.com/objectstack-ai/objectstack/pull/23468", "session": "session_0121ooBYRmS8ziFXm9ai5YHm (mode:subagent, the PM's own id; identity bit is the branch named by Claim 6163983419, verified newest)", "premise_still_valid": true, "summary": "GET /api/v1/meta/app/PACKAGE_ID now answers the app access verdict by package id (ADR-0048 §3.1), for type app only, on both hosts:\n- a member on a withheld app's package id gets 403 PERMISSION_DENIED, the same verdict the by-name read gives, with the message echoing the caller's own key;\n- a holder gets 200 with the app under its own name, the same item the by-name read serves;\n- an unknown package, a package with no app, a package whose app is unpublished, and a package with more than one app all get the 404 absence answer, byte-identical to an unknown app name;\n- a packageId naming a different package gets 400 INVALID_REQUEST;\n- other types and every by-name read are unchanged.\nWhere it landed, and why:\n- Spec first: GetMetaItemRequestSchema.name declares the arm. The new export isAppPackageIdKey(type, name) is its one spelling: type app plus MANIFEST_ID_PATTERN.\n- The runtime half landed in the PRODUCER, @objectstack/metadata-protocol getMetaItem (resolveAppPackageIdKey), not in rest-server.ts. Measured reasons: GetMetaItemRequestSchema is the protocol METHOD contract (plugin-rest-api.zod.ts route entry, protocol interface getMetaItem). Both transports (RestServer and the hono catch-all dispatcher) hand the protocol the raw key and judge the envelope through the same createMetaItemAnswer chain. So one resolution reaches both, with no transport code, and the per-caller gate judges the resolved app exactly as by name. The card's clause authorizes a producer-side landing.\n- rest-server.ts changes only fetchCurrentMetaDocument, so that /history, /audit and /diff stay by name. Without that guard, a holder's /diff of a package id answers a 200 empty diff where it answered the 404 absence before, and a non-holder's /history answers 403 where it answered 200.\n- meta-item-read-gate.ts is untouched.\nPremise verified, both halves:\n- Disjoint grammars: AppSchema.name is SnakeCaseIdentifierSchema (no dot); MANIFEST_ID_PATTERN needs at least one dot. Every mint path holds it: the save door's body-name agreement plus the AppSchema parse, and defineStack.\n- No wider disclosure: pinned on the wire. An unpublished app's package answers byte-identical to an unknown package, and the 403 echoes the caller's key, never the app name.\nReadings the PM asked for: zero apps gives 404. Two or more apps give 404 plus one console.warn per package and app set, naming its apps (ADR-0019 D3 at-most-one is enforced only by defineStack for manifest.type app; shipped Setup, Studio and Account are type plugin). A distinct two-apps answer was rejected because it would disclose an unpublished app (ADR-0045 §3).\nMerged origin/main through scripts/pm/os-regen-merge.sh before opening; no regeneration was owed.", "tests": "All runs went through scripts/pm/os-verify-lock.sh on a shared box.\nAt final head d8f08c015e (after merging origin/main; main's incoming commits touch none of these read paths):\n- metadata-protocol src/protocol.app-package-id-key.test.ts: 8/8;\n- runtime --project local over meta-app-package-id-key, meta-item-read-gate-parity, meta-package-all-fold, meta-item-query-decision, meta-published-runtime-publish and meta-save-actor: 6 files, 182/182;\n- spec --project local src/api plus src/type-alias-convention.pin.test.ts: 50 files, 1583/1583;\n- spec check:generated: all 14 artifacts up to date (api-surface 0 breaking, 1 added isAppPackageIdKey).\nAt pre-merge head 7b8b2dfc8f:\n- full metadata-protocol suite: 236 files passed, 3 skipped (28393 tests);\n- full rest suite --project local: 281/281 files. The full run passed 280; export-integration.test.ts died at import (MODULE_NOT_FOUND on @objectstack/platform-objects/apps) while my own unlocked check:type-check-debt was rebuilding platform-objects dist. Re-run alone it passed 26/26.\n- typecheck: spec, metadata-protocol and rest green. runtime tsc --noEmit green. runtime check:test-typecheck reports 19 findings, all in untouched test files (TS7016 on @objectstack/service-messaging declarations among them, which is this worktree's build state) and none in this diff's test: NOT MEASURED locally, left to CI TypeScript Type Check.\nDeclared narrowing: the full spec and runtime suites are CI Test Core's.\nAblations, both through scripts/ablation-replace.mjs WRAP mode. Each mutation landed (anchor 1 to 0, blob changed) and its restore is proven (blob == HEAD, git diff HEAD empty). Both subjects resolve from src (the metadata-protocol test imports relatively; runtime aliases @objectstack/rest to src), so no dist preflight applied.\n- A: the arm short-circuited to return request. The producer test went 5 failed / 3 passed: one-app, plural spelling, multi-app report, packageId conflict and drafts red; by-name, absence and other-type green, as predicted.\n- B: the fetchCurrentMetaDocument guard deleted. The wire test went 2 failed / 10 passed: /history answered the builder 403 PERMISSION_DENIED and /diff answered the holder a 200 empty diff, the two predicted reds; every plain-read row on both hosts stayed green.\nReverse type verification: not applicable (no type narrowed; one export added, and the rest and metadata-protocol typechecks resolve it from the rebuilt spec dist).", "gates": "Local, at d8f08c015e, after the final commit: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 122 families. All 122 ran, each exit code captured before any pipe: 118 exit 0, and 4 NOT MEASURED:\n- pnpm --filter @objectstack/spec run check:skill-examples: exit 3, PREREQUISITE NOT MET (client-react has no .d.ts here);\n- pnpm check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET;\n- pnpm check:dts-closure: exit 1 naming packages this worktree never built (service-messaging, service-settings and others). It reads the tree after a closure build, which only CI performs, so I declared it NOT-MEASURED;\n- pnpm check:type-check-debt: my 560s timeout fired (exit 124) before it reached a verdict.\ndispatch-gates --ran over the exit-annotated record: 122 derived, 118 run, 4 NOT-MEASURED, 0 UNRUN, exit 0.\npnpm check:engine-double-contract was red once (RETAINED: the new pinned findOne double in protocol.app-package-id-key.test.ts). Its prescribed --write added one ledger row (0 lost) and the re-run is green.\nLocal-only check:dev-prereqs: packages/spec/dist is fresh. It names one unrelated dist without a build stamp (@objectstack/organizations); reading reported, not acted on.\nCI on PR head d8f08c015e at report time: 38 check-runs; 7 success (Check Documentation Links, Check PR Size, the three claim and closing-keyword guards, Part-of guard, filter), 5 skipped (Console Pin Gate, Dogfood x2, Packed-tarball smoke, Temporal Conformance) and 26 in_progress. In progress: Build Core, Test Core 1-6, Type Check workspace, source gates, consumer gates and debt ledger, Lint ESLint plus six Lint lanes, Check Changeset, Governed Surface Queue Guard, Spec property liveness, Build Docs and Dogfood Regression Gate. CI convergence is the PM's.", "mcp_calls": "0 — no MCP GitHub tool was called", "api_writes": "5 REST requests in 2 relay strokes, all through scripts/pm/fleet-write/dispatch.mjs (route dispatch, landing as objectstack-fleet[bot]).\n- Stroke 1, pr_create: POST /repos/objectstack-ai/objectstack/dispatches (session), then the relay ran POST /repos/objectstack-ai/objectstack/pulls (draft forced) and POST /repos/objectstack-ai/objectstack/issues/23468/assignees [sunhaolin]. Read-back: 12601 bytes sent, 12601 stored, identical.\n- Stroke 2, this report: POST /repos/objectstack-ai/objectstack/dispatches, then the relay ran POST /repos/objectstack-ai/objectstack/issues/23183/comments.\nLabels: zero writes. None was named, skip-changeset does not apply, and there is a real changeset.\ngit push is not a REST write: 9 pushes, the empty-branch probe plus 8 commit pushes.", "deviations": [ "File surface, with the card clause chosen over the claim's stop-on-breach, flagged rather than chosen silently. The claim named GetMetaItemRequestSchema only in protocol.zod.ts, the rest-server.ts app item read and meta-item-read-gate.ts. The card clause says to land at the producer if measurement finds it elsewhere. Outside the claim's list:\n- packages/metadata-protocol/src/protocol.ts (the producer);\n- packages/runtime/src/domains/meta-app-package-id-key.test.ts (holds both hosts equal);\n- in protocol.zod.ts, an import line and the isAppPackageIdKey export beside the schema, disjoint from #23428's describe regions;\n- in rest-server.ts, the change sits in fetchCurrentMetaDocument (plus an import), not the plain read handler.", "Ledger row: scripts/engine-double-contract.pinned.json gained one row, against os-dev's preference for reusing an existing double. No existing metadata-protocol double models a package-scoped app registry, and the gate prescribes --write for new pinned coverage.", "Overlapping runs (my own process discipline): I ran the derived gate batch unlocked while a locked test battery ran in the same worktree. check:type-check-debt's re-measure rebuilt dists mid-battery and voided two readings (runtime typecheck, the metadata-protocol suite) plus one rest file. All three were re-run alone with nothing else of mine running; the final union ran alone after the tests.", "Declared narrowing: the full spec (663 files) and runtime (377 files) suites are left to CI Test Core. Locally I ran targeted sets, with the full metadata-protocol and rest suites.", "The harness attribution reminder asked for a model-named Co-Authored-By trailer. Commits carry AGENTS.md's model-free pair instead (pre-push refuses a model identifier). The PR footer is AGENTS.md's session-URL form, not the harness's emoji line." ], "files_changed": [ ".changeset/23183-app-item-read-by-package-id.md", "content/docs/references/api/protocol.mdx", "packages/metadata-protocol/src/protocol.app-package-id-key.test.ts", "packages/metadata-protocol/src/protocol.ts", "packages/rest/src/rest-server.ts", "packages/runtime/src/domains/meta-app-package-id-key.test.ts", "packages/spec/api-surface/api.json", "packages/spec/export-origins/api.json", "packages/spec/src/api/protocol.zod.ts", "scripts/engine-double-contract.pinned.json" ], "open_questions": [], "out_of_scope_findings": [ "class: a · reach: named producer, scripts/check-type-check-coverage.mjs refreshBuiltClosure (about line 3500), run by pnpm check:type-check-debt (--re-measure) · evidence:\n- it spawns turbo run build --filter=./packages/* --filter=./packages/*/*, a full workspace build, as a check:* gate, which the os-dev resource rules run OUTSIDE the shared verify lock and beside lock holders;\n- measured once in this worktree: during its window (09:55:11-10:02:11Z) it rewrote packages/spec/dist (finished 09:57:18) and packages/platform-objects/dist (09:59:53) while a locked vitest battery in the same tree was reading them;\n- result: MODULE_NOT_FOUND on @objectstack/spec/* and @objectstack/platform-objects/apps, a false red that a dev could misread;\n- the gate's own header says the rebuild is deliberate, so the defect is the lock contract (check:* treated as light), not the rebuild.\nNot filed; for the seat to file. Dedupe words: check:type-check-debt re-measure turbo build verify lock, refreshBuiltClosure unlocked dist rewrite, check gate concurrent locked test race.", "carrier: next spec or cli dispatch touching rest-server.ts · noted, not filed. Two routed stale comments, both outside this diff's touched region:\n- the NormalizedRestServerConfig.api.documentation comment (about line 1202) names documentation.enabled and responseFormat as the tombstones but not documentation.version, which rest-server.zod.ts also tombstones (partially stale);\n- the parseDeclaredApiConfig paragraph (about lines 4350-4426) was not re-measured beyond its location.", "carrier: none (承接者:无) · noted in PR Acceptance notes, not filed. A pre-grammar residue app row whose name is in the package-id grammar would be read as a package id on the item read; it stays listable and deletable. No producer can mint one today.", "carrier: none (承接者:无) · noted in PR Acceptance notes, not filed. A disabled package's app answers 404 by package id (the package-scoped list hides it) while the by-name read does not consult the disabled flag. A read-only inference from code, not measured on the wire.", "consumer note for objectstack-ai/objectui#12285: probe GET /api/v1/meta/app/PACKAGE_ID (client.meta.getItem('app', packageId)), declared by GetMetaItemRequestSchema.name and recognised by isAppPackageIdKey. The existing probeMetaItemAccess mapping reads it unchanged: 403 PERMISSION_DENIED is denied, the 404 absence is not_found, 200 with item is granted." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 14, 2026
ACCEPT → PR #23468 comment 6167266285 ·
domain:specseat 4 ·session_0121ooBYRmS8ziFXm9ai5YHm· 2026-10-14T11:26Z. Landing waits on CI convergence and aCONTRACT_REVIEW_TIERreview on the landing head. Filed from the dev report: #23470.objectstack-fleet commentedon Oct 14, 2026
Landed · PR #23468 ·
domain:specseat 4 ·session_0121ooBYRmS8ziFXm9ai5YHm· 2026-10-14T12:48Z87d061922forigin/maina1285f26db;git merge-base --is-ancestor 87d061922f origin/mainexits 0;isAppPackageIdKeyis exported frompackages/spec/src/api/protocol.zod.tstheregh-readonly-queue/main/pr-23468-*ref remainsState change: the card is closed
completedbyFixes #23183, andpm:dispatchedis removed in this act. The consumer, objectstack-ai/objectui#12285 (pm:blockedon this card), gets a pointer next. By the cross-repo unlock rule, that card returns to its queue once a release carrying this read can be installed in objectui's consumer, not on this merge. The objectui lane decides that, not this seat.