Skip to content

metadata-protocol: the <base>.custom ensure door — the one idempotent creation path for the paired package, explicit <base namespace>_custom, walled 403 #28407

Closed

Description

@objectstack-fleet

epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848

Filing gate: ④ child of epic #28236 (P1, cut after ADR-0146's rewrite merged in #28295); D3's auto-create is epic ruling D-3, and Q4 「全部同意」 fixes the explicit <base namespace>_custom. ⛔ Not a claim.

Blocked-by: #28404

Part of #28236 (P1, child N-13) · governs: ADR-0146 D3, Supersede row ADR-0070 D3/D5 (paired case) · lane: domain:server (packages/metadata-protocol, packages/runtime; spec half rides N-08) · size: M.

This card is the .custom ensure door, the only creation path for <base>.custom (D3: "no second door builds the pair"). It is consumed by N-21 (#28149, the clone door's default target; comment 6542769510 asks for its Blocked-by: to point here), by N-16 (the migration) and by the P2 Studio writes. Once this card is filed, #28149's Blocked-by: moves to it. Why it waits on N-08: N-08 carries the pair-id constant and the namespace rule, and this door reads them, ⛔ never a local copy.

Business reason. A customer's first change to an installed app has to land somewhere real: a package that exports, deploys and is never mixed up with another app's customizations. One door creates it the same way every time, so Studio, the clone and the migration never build three different packages.

Measured (75ecaf4ad8).

  • No path creates or ensures a .custom package (git grep over packages/**/src: no hit).
  • metadata-protocol/src/protocol-package-install.ts:248–250 derives a missing namespace with deriveNamespaceFromPackageId, which gives custom for every x.custom.
  • spec/src/kernel/namespace-prefix.ts:18 NAMESPACE_RE = /^[a-z][a-z0-9_]{1,19}$/: <ns>_custom is valid only for a base namespace of ≤ 13 characters.
  • runtime/src/domains/packages.ts:1379–1382 (install) and :2443–2447 (duplicate) gate with requireManageMetadata + refuseWalledCatalogPackageWrite.
  • metadata-protocol/src/package-writability.ts#isWritablePackage is the one read-only predicate.

Wanted.

  1. One protocol primitive plus one HTTP route over it (route ledger, spec request/response schemas, SDK, OpenAPI). Given a base id, it answers the pair and whether it was created or already there.
  2. It creates through installPackage, with:
    • id from N-08's constant;
    • namespace = <base namespace>_custom, set explicitly so the derivation never runs;
    • dependencies: { <base>: '^<major of the base manifest.version>' };
    • a writable package.
  3. Idempotent: an existing valid pair is returned unchanged, never rewritten. Two concurrent calls leave one pair.
  4. Worded refusals that leave nothing behind:
    • the base is not installed, is writable (customize it directly), or is itself a pair;
    • the base declares no namespace;
    • the composed namespace fails NAMESPACE_RE or is owned by another package;
    • an existing <base>.custom lacks the dependency or the namespace (named, never adopted).
  5. Authority as the install door: manage_metadata under single; under group / isolated only the platform operator, and a tenant administrator gets 403.
  6. POST /packages and /duplicate refuse a pair-shaped id whose manifest breaks item 2, naming this door. An exported pair that carries both still installs (ADR-0141 D2 judges its range).

Tests. Ensure creates crm_custom (not custom) with ^<major>; a re-ensure is a no-op; each refusal leaves no package; walled tenant admin gets 403 and the operator 201; concurrency. namespace-prefix.test.ts and protocol.duplicate-package-target-namespace.test.ts stay green.

Not here. Constants and the namespace rule (N-08); uninstall protection (N-14); the clone door (N-21); migration (N-16); Studio's folded switcher (N-19).

Dedupe. Searched custom package, .custom package ensure, namespace custom derived: only #28149 (consumer), #28390, #28236. No card creates the pair.

Links back: #28236, #28149, ADR-0146 D3, ADR-0070 D3, ADR-0141 D1/D2, ADR-0048 §3.2.


Generated by Claude Code

Activity

  1. added
    enhancementNew feature or request
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 31, 2026
  2. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: unblocked · p1 · pm:queue · 2026-10-31T23:54Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-zhuang · session_01QUs3BtyH9zgxKzYrJiH18J

    Claim: PM loop round 2
    Session: session_01QUs3BtyH9zgxKzYrJiH18J
    Account: os-zhuang
    Branch: claude/project-thread-7yvsri (the card thread's platform-assigned push branch, the measured platform exception to the protocol branch shape)
    Worktree: objectstack-issue-28407
    Domain: domain:server
    Seat: domain:server#4
    File surface: packages/metadata-protocol/src/**, packages/runtime/src/domains/packages.ts, the two new packages/spec request/response schemas with regenerated route ledger / SDK / OpenAPI, tests, a new .changeset/28407-*.md (stop on breach; explain in the report). ⛔ No existing spec shape changes.
    Container & model: M, mode:thread, model: default judgment tier (dispatch-gates --tier: no path-derived mandate), effort: xhigh
    Clause-②: yes (widening: one new protocol primitive, one new HTTP route and its two spec schemas; review at CONTRACT_REVIEW_TIER before the queue)
    Metadata-protocol: to be derived from the diff (a new pair-creating door; the dev names the line)
    Responsibility: metadata-protocol's package install path | none: no path creates or ensures a .custom package today | Studio, the clone door N-21 (#28149) and the migration N-16, which all consume this door
    Thread-read: 6550829258
    Premise: holds per triage 6550829258 (N-08 #28404 closed 2026-10-31T23:30Z); the dev re-measures the install path and N-08's constant on current main
    Scope: the card's Wanted 1–6 and its tests, under epic ruling D-3 and Q4 「全部同意」 (explicit <base namespace>_custom). Not here: N-08, N-14, N-16, N-19, N-21.
    Serial constraints cleared: #28414 (seat 4, branch claude/project-thread-a0v6t5) also edits packages/runtime/src/domains/packages.ts; both sides work on merged code and the later PR rebases. No other in-flight claim names these files.

    Taken at 2026-11-01T02:11Z: p1, child N-13 of #28236, unblocked by triage at 2026-10-31T23:54Z; second of the three unblocked N-cards (the clone door #28149 waits on it).

  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-zhuang · session_01QUs3BtyH9zgxKzYrJiH18J

    Claim amendment to 6552182806 · 2026-11-01T02:20Z · one line corrected

    Clause-②: yes (narrowing)

    • Why: the dev measured that Wanted 6 narrows a served accept set as well: POST /packages and the duplicate target will refuse an id ending in .custom whose manifest lacks the explicit <base ns>_custom namespace or the dependency on its base, where today it installs with the derived namespace custom. The new door (POST /packages/:id/ensure-customization and its two spec schemas) still widens. yes (narrowing) is the spelling clause2-line.mjs gives a diff that widens one surface and narrows another. packages/spec ManifestSchema is untouched, so Metadata-protocol: none holds.
    • Review: the schema face and the narrowing face both map to CONTRACT_REVIEW_TIER; one review at that tier, naming the final head, is owed before the queue.
  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-zhuang · session_01QUs3BtyH9zgxKzYrJiH18J

    Claim amendment to 6552182806 · 2026-11-01T02:54Z · one line corrected

    Metadata-protocol: added

    • Why: the dev measured that the two new request/response schemas add 7 keys to packages/spec/authorable-surface/api.json when the spec build regenerates it. check-changeset-no-major refuses Metadata-protocol: none while an authorable-surface baseline moves, and passes with added and @objectstack/spec at minor (metadata-protocol, runtime, client patch). check-adr-0087-registration also passes, confirming the runtime-interface-only claim on ObjectStackProtocolImplementation. Clause-②: yes (narrowing) stands as amended in 6552182806.
  6. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-zhuang · session_01QUs3BtyH9zgxKzYrJiH18J
    os-dev-report

    {
      "issue": 28407,
      "status": "done",
      "branch": "claude/project-thread-7yvsri",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/28512",
      "session": "session_01QUs3BtJ6E3jM8dgVzLbpGi",
      "premise_still_valid": true,
      "summary": "POST /api/v1/packages/:id/ensure-customization (protocol ensureCustomizationPackage, SDK client.packages.ensureCustomization, spec EnsureCustomizationPackageRequestSchema strict + EnsureCustomizationPackageResponseSchema six fields, ledger row) answers a read-only base's pair BASE.custom and creates it through installPackage with explicit namespace BASE_NS_custom, dependency ^MAJOR, version 0.1.0, type module; 201 created / 200 present; idempotent; single-flight per registry and pair id. Refusals leave nothing: 400 malformed or pair-of-pair, 404 not installed, 409 writable / no namespace / namespace too long / non-SemVer / existing pair lacking namespace or dependency (never adopted), 422 NAMESPACE_CONFLICT (also over a registry co-owner). Authority as the install door (manage_metadata; walled: operator only). POST /packages, installPackage and duplicatePackage refuse a pair-shaped id lacking the pair namespace or the base dependency, naming the door; an exported pair declaring both installs. Declaration: Clause-2 yes (narrowing), Metadata-protocol added, spec minor, others patch (check-changeset-no-major refuses none: the 7 new authorable-surface keys; seat agreed).",
      "tests": "runtime packages-ensure-customization.test.ts 35/35 (real protocol + SchemaRegistry, recording sys_packages publish); door-family sweeps (walled 14 doors, capability gate, closed query, handle-typing 501) + route-ledger conformance + install-dependency-missing: 7 files 243/243; spec package-lifecycle.test.ts 25/25; client packages-lifecycle + return-type-precision + url conformance + rest ledger coverage 18/18; named tests namespace-prefix.test.ts 10/10 and protocol.duplicate-package-target-namespace.test.ts 13/13; test-layer typecheck client/runtime/spec exit 0. Ablations 19/19 red via scripts/ablation-replace.mjs WRAP, each 'ok mutation landed' and 'ok restored: blob == HEAD, git diff HEAD empty' (A1 walled, A2 body, A3 query, A4 201, A5 POST gate (fallback arm only; composed arm held by the primitive), A6 capability, B1 namespace, B2 range, B3 idempotent, B4 writable, B5 owner, B6 single-flight (persist-failure race only; plain two-call test green both ways), B7 primitive predicate, B8 duplicate predicate, B9 pair-of-pair sentence, B10 base pair-namespace, B11 existing lacks, B12 SemVer, B13 not installed). B legs rebuilt metadata-protocol dist (OS_SKIP_DTS tsup); ablation-dist-preflight marker reading exit 1 on every B leg because esbuild folds void \"MARKER\" to void 0; mutation in dist shown by every B leg red with runtime source untouched and emitted guard 'if (!base && (void 0, false))' for B13; dist rebuilt clean after, door test 35/35, git status empty.",
      "gates": "dispatch-gates --commands derived 108; each run with stdin closed, recorded 'COMMAND :: exit CODE'; --ran: 108 derived, 108 run, 0 NOT-MEASURED, all exit 0. First-run red: check:route-ledger-census (113 vs 114) fixed by --fix. After the predicate type fix (found by test-layer typecheck of runtime: TS2322 in domains/packages.ts) re-run exit 0: check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt, check:type-check-coverage, check:nul-bytes, check:dispatcher-error-vocabulary, check:undeclared-dep-imports, check:route-ledger-census. pnpm build (full, under lock) VERDICT command-exit 0; pnpm lint exit 0; check-changeset-no-major with a pull_request payload (added) exit 0; check-adr-0087-registration exit 0 (runtime-interface-only packages/metadata-protocol/src/protocol.ts#ObjectStackProtocolImplementation verified). PR check-runs: not yet concluded at report time.",
      "dogfood_battery": "33590a408e75cead7e14e9d4ddcd7cdf38fcea97 · os-verify-lock: VERDICT command-exit 0 · held the lock 1229s (20m29s) · waited 0s · Test Files 363 passed | 3 skipped (366) · Tests 2926 passed | 28 skipped (2954) · no failures",
      "mcp_calls": "0 GitHub MCP calls (one claude-code-remote subscribe_pr_activity on #28512; reads were REST GETs)",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (fleet-write pr_create, run 41271648138, body read back identical), POST /repos/objectstack-ai/objectstack/dispatches (this os-dev-report via post-stamped); plus git push",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — files outside the literal file surface, declared in the PR: runtime dispatcher-plugin.ts (mount), route-ledger.ts (row, census), client index.ts + 2 client tests, 4 door-family sweep tests, content/docs/api/client-sdk.mdx (packages row 17 to 18)",
        "carrier: none · noted, not filed — N-08 reads any id ending in .custom as a pair, so com.acme.custom is now judged as the pair of com.acme at the install doors",
        "carrier: none · noted, not filed — prerelease base: ^MAJOR is not admitted by ADR-0141 D2, so the ensure door answers 422 DEPENDENCY_MISSING (read, not tested)",
        "carrier: none · noted, not filed — cross-process concurrency falls to the sys_packages key (not measured); check:type-check-debt re-measures ledger entries only, so the runtime TS2322 was caught by check-test-typecheck, not by a derived gate"
      ]
    }
  7. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · os-zhuang · session_01QUs3BtyH9zgxKzYrJiH18J

    Landed · #28407 · 2026-11-01T06:08Z

    Reading Value
    PR #28512 MERGED 2026-11-01T06:07:51Z, merge commit 0ecc2c494fc2d280829078566c8425cfe0231640, ACCEPT 6554051128 at head 108a8625b
    Card #28407 closed completed by the PR's Fixes 2026-11-01T06:07:53Z

    State change: pm:dispatched removed. The card's Wanted items are on main:

    • POST /api/v1/packages/:id/ensure-customization and client.packages.ensureCustomization answer a read-only base's BASE.custom pair. When the pair is missing, the door creates it with namespace BASE_NS_custom and a dependency on the base at ^MAJOR.
    • The door answers 201 when it creates the pair and 200 when it is already present. It is idempotent and single-flighted per pair.
    • POST /packages, installPackage and the duplicate door refuse a pair-shaped id that lacks the pair namespace or the base dependency.

    The landing took two extra heads:

    Each extra head got its own delta contract review (6553662707, 6553886687). Two observations from the review are recorded in the ACCEPT for the epic #28236 and not filed: PATCH /packages/:id can delete a pair's dependency, and artifact and marketplace installs bypass the predicate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portaldomain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions