Skip to content

.custom export installed on another environment reports success and the override is not applied #30106

Closed

Description

@objectstack-fleet

epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

Found in a real-user walkthrough of the development flow, asked for by the maintainer: 「你站在真实用户实际使用的角度完整的走一遍具体的开发流程,再看一下还有什么问题」 (2026-11-04).

Governing text: docs/adr/0146-*.md D2: a customization is "exported (GET /packages/:id/export), committed, reviewed and deployed to another environment as any package is"; the Dependency range note names "an export of hotcrm.custom deployed into another environment".

What a customer sees

An administrator customizes a view on staging and moves it to production with the documented commands. The CLI prints a success message and production keeps showing the old view. Nothing says the change was dropped.

Reproduction

Project scaffolded with create-objectstack@18.3.0, one code package com.example.asset-repair, view asset_repair_ticket.open.

  1. On the source instance, POST /api/v1/packages/com.example.asset-repair/ensure-customization, save the view into com.example.asset-repair.custom with overrides: { package, basedOn } and a new label, then publish-drafts. The instance serves the new label.
  2. os package export -u SOURCE -o ./export. The file 01-com.example.asset-repair.custom/objectstack.json carries viewItems with the new label and the overrides block.
  3. os package install ./export/01-com.example.asset-repair.custom/objectstack.json -r TARGET --email OWNER on a fresh os start instance that runs the same base package. It prints ✓ Package installed into the running kernel.
  4. On the target, GET /api/v1/meta/view/asset_repair_ticket.open.

Measured

Read on the target Result
GET /api/v1/meta/view/asset_repair_ticket.open The base label, no overrides, package com.example.asset-repair
GET /api/v1/packages/com.example.asset-repair/customizations counts.overrides: 0
GET /api/v1/packages Both packages enabled: true, status: installed
Server log [Registry] Registered view: com.example.asset-repair.custom:asset_repair_ticket.open

Same result on the published 18.3.0 and on main 63b86b385d with the same exported file. On 18.3.0 a server restart does not change it (not repeated on main).

Only view was measured. Other overridable types were not run through this path.

Expected

After the install the target serves the override, and the customizations read lists it, the same as on the source. If the install cannot apply an item it says so and does not report success.

Activity

  1. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

    The maintainer asked for more testing here: 「继续测试」 (2026-11-04). Measured on main 987ff33b88, CLI and server built from that commit. This widens the card: the install also locks the target out of further customization.

    Result by item type

    Source: os dev, one .custom holding the items below, published and served there. Export: os package export. Target: a fresh os start, separate database, same base package. Install: os package install FILE -r TARGET --email OWNER, which prints ✓ Package installed into the running kernel.

    Item in the exported file On the source On the target after install After a target restart
    Override, view Served Not served, base label Not served
    Override, dashboard Served Not served Not served
    Override, app Served Not served Not served
    Override, dataset Served Not served Not served
    New item, view under its own name Served Served Served

    GET /api/v1/packages/com.example.asset-repair/customizations answers overrides: 4 on the source and overrides: 0 on the target. The exported file does carry all four with their overrides block.

    The install also closes the door behind it

    Step on the target, after the install Result
    POST /packages/com.example.asset-repair/ensure-customization 200, created: false, writable: false
    Save a view override into the pair, as Studio does 422 Cannot create view/… that package is read-only (provided by code or an installed app)
    DELETE /api/v1/packages/com.example.asset-repair.custom 422 WRITABLE_PACKAGE_REQUIRED … it is read-only
    os package install FILE --install-mode unmanaged 422 INSTALL_MODE_NOT_PERMITTED: the file names no installModes, so only managed is allowed

    So a customer who follows the documented path ends with no change applied and no way to customize that base on that instance. The only recovery the seat found is deleting the cached file by hand from the host, and restarting.

    The same install on a target whose pair already exists and is writable also reports success, and the overrides still do not apply. The seat did not read whether rows already in that pair survive or are shadowed.

    Where the cached file lives

    The install wrote .objectstack/installed-packages/com.example.asset-repair.custom.json under the server's working directory even though that server ran with OS_HOME set elsewhere. A later instance started from the same directory with a different OS_HOME and a fresh database picked the package up. This was found by accident, when one test contaminated the next. A container's working directory is usually not on a volume, so whether the install survives a container replacement was not measured.

    Likely cause, from reading the code

    • Every read of an override asks sys_metadata rows: listServedOverrides in packages/metadata-protocol/src/protocol-served-overrides.ts selects where: { package_id: pair.value, state: 'active' }.
    • The install registers the file's items in the in-memory registry as a code-provided package and writes no such row. The server log shows [Registry] Registered view: com.example.asset-repair.custom:asset_repair_ticket.open.

    That explains both halves: no row means no override served, and a registered package counts as provided by code, so it is not writable. This is a reading and was not confirmed by a fix.

    Other door tried

    POST /api/v1/packages does not take the exported file: 400 on the missing manifest.type, and with a type added, 400 Unrecognized key(s)… apps, dashboards, datasets, viewItems. So no other published door was found for carrying a .custom between environments.

    Not measured

    • Other overridable types: report, translation, email_template, page, doc, book, mapping.
    • Object extensions (__c fields) carried in a .custom.
    • The Docker path, because Docker is not running on this machine.

    Expected, restated

    ADR-0146 D2 says the pair is "exported, committed, reviewed and deployed to another environment as any package is". After the install, the target serves the overrides, the customizations read lists them, and an administrator there can keep customizing. If an item cannot be applied, the install says so and does not print success.

  2. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p1 · domain:server · pm:queue · 2026-11-04T14:53Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

    Claim: PM loop round 1
    Session: session_01Utp6CbPxYq1wQmUNWvMZkC (seat) · dev = project card thread session_01Utp6CXfNPQKEBNuyohXjfg
    Account: os-bill
    Branch: claude/project-thread-j915rv
    Worktree: objectstack-issue-30106
    Domain: domain:server
    Seat: domain:server#3
    File surface: packages/cli/src/commands/package/**, packages/runtime/src/domains/packages.ts (and the install door it calls), packages/metadata-protocol/src/protocol-served-overrides.ts read path only if the fix needs it, packages/cloud-connection/src/local-manifest-source.ts (cache location) — stop on breach; explain in the report. ⛔ packages/spec.
    Container & model: M–L, mode:thread, model: default judgment tier (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable), effort: xhigh (p1)
    Clause-②: no — the fix makes the install apply what the published export already carries (ADR-0146 D2); the dev re-measures on the built entry closure and reports.
    Metadata-protocol: none
    Responsibility: the package install door registers a .custom file in memory as code-provided and writes no sys_metadata row (reading, not confirmed) | no platform path carries a .custom between environments today (POST /packages refuses the file, 6633535319) | any customer following the documented ADR-0146 D2 deploy path; reached on 18.3.0 and main
    Thread-read: 6633829810
    Premise: holds
    Serial constraints cleared: #30062 (declared overrides, metadata-protocol, unclaimed in pm:queue) may intersect at metadata-protocol; the dev merges main before opening the PR. No in-flight claim on these paths.

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CXfNPQKEBNuyohXjfg
    os-dev-report

    {
      "issue": 30106,
      "status": "done",
      "branch": "claude/project-thread-j915rv",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30207",
      "session": "session_01Utp6CXfNPQKEBNuyohXjfg",
      "premise_still_valid": true,
      "summary": "PM hypothesis measured true, and one fact is added. On main 07595c07 the managed install-local path registered the pair as code. No sys_metadata row was written, so listServedOverrides and the served read saw nothing. The pair also turned read-only (scope cloud, engine.manifests), so ensure-customization answered writable:false and the next save into it got 422 WRITABLE_PACKAGE_REQUIRED. The CLI still printed success. The fix routes a pair id (customizationBasePackageId) to installCustomizationPair: the target's ensure door, then saveMetaItem per item (mode publish, pair as package, installer as actor), in load order. Nothing is registered or ledgered. A pair that is already read-only is refused 409 with its way back; a refused save answers failure, and a pair this install created is rolled back. The CLI prints the pair, and prints an older runtime's as-code answer as an error (exit 1).",
      "tests": "At 6be0aea6a: @objectstack/cloud-connection full vitest, Test Files 70 passed (70), Tests 846 passed (846). New: customization-pair planner (7 cases) and customization-pair-install (15 cases). Updated: override-gate (3 cases) and extension-field-suffix (1 case), from registered===[PAIR] to saves named. @objectstack/cli --project unit: 382/383 files and 5381/5382 tests. The one failure, package-export.test.ts (its stub answered the pair as code), was fixed and passes 6/6; new package-install-customization-pair 2/2. Dogfood package-export-reinstall-custom-pair + package-export-bundle-round-trip: 2 files, 16/16 tests. cloud-connection typecheck exit 0; cli tsc --noEmit exit 0 (its test-typecheck OOMed while two dev servers ran, so NOT MEASURED). Dev e2e on two os dev showcase runtimes, before and after the fix, with readings in the PR Verification section. After the fix the override labels are served, customizations overrides:2, ensure writable:true, a further save into the pair 200, re-install idempotent, no ledger file, rows persist across two restarts. Stale code pair: 409 naming DELETE + restart, then 409 naming restart, then success after restart. Ablation (scripts/ablation-replace.mjs; tests import src, so no dist): the pair branch was disabled ('if (pairBaseId !== null)' changed to 'if (false && pairBaseId !== null)'; anchor 1 to 0, blob 304abdb60d58 to bef2069a57a3). customization-pair-install + override-gate + extension-field-suffix went from 30 passed to 18 failed / 12 passed. Restored: blob == HEAD, git diff HEAD empty.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 64 commands at 6be0aea6a, each run with its exit code recorded before any pipe: 64 :: exit 0. --ran: 64 derived famil(ies) accounted for, 64 run, 0 NOT-MEASURED. On the first pass, pnpm check:durability-log-level exited 1 (the save-loop catch had no log; fixed in place by an error log in the catch) and check-plugin-teardown-shape --self-test exited 3 (shallow clone; pinned fixture commit fetched). pnpm lint exit 0. The PR's check-runs had not been read when this report was written.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm declared. The two pair dogfood files ran (16/16); see open_questions on the Clause-② re-check",
      "mcp_calls": "5 — mcp__github__issue_read x5 (reads); no MCP write tool used",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (relay pr_create, landed #30207, body identical, assignees none); this comment via post-stamped. git push is not a REST write.",
      "open_questions": [
        {
          "question": "Clause-② re-check (dev 实测复核). The claim's line, copied, is 'no'. Measured: a pair-id body carrying a key with no item form (code, a translations bundle, an envelope, an unclassified key), or a pair inside a multi-package artifact, is now refused, where before it was registered as code. A pair install's answer gains customizationPair / writtenCount / written and no longer carries hotLoaded / storageDir. Keep 'no'?",
          "options": [
            "A keep Clause-②: no — the refused bodies never installed a working pair (nothing was served), and the answer keys are additive for one id class",
            "B Clause-②: yes (narrowing) — say the narrowed accept set in the PR body and the changeset; the seat edits both"
          ],
          "recommendation": "A, because a pair registered as code served none of what it carried, so the refused shapes had no working install to narrow; the seat owns the call"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: a · reach: DELETE /api/v1/marketplace/install-local/ID answers 200 'withdrawn from the running kernel', then POST /api/v1/packages/BASE/ensure-customization answers 201 created:true writable:false — the engine's manifests map keeps the id until a restart, so an uninstalled code package still reads read-only (measured on pre-fix and fixed builds) · evidence: package-writability.ts isWritablePackage reads engine.manifests first; engine.ts only ever sets it · dedupe words: install-local uninstall manifests writable, ensure-customization writable false after uninstall, engine.manifests never deleted",
        "class: a · reach: os dev --fresh then a managed install-local writes CWD/.objectstack/installed-packages/ID.json (measured: examples/app-showcase/.objectstack/installed-packages/com.example.showcase.custom.json under a fresh OS_HOME); an entry there is rehydrated by the next --fresh boot (measured) · evidence: local-manifest-source.ts default resolve(process.cwd(), DEFAULT_INSTALLED_PACKAGES_DIR); serve.ts constructs the plugin without storageDir; dev.ts says state not keyed off OS_HOME is not covered · judged out of scope: a pair no longer writes one; moving it for other packages needs a read-old-location fallback · dedupe words: installed-packages OS_HOME, LocalManifestSource cwd, --fresh install-local cache",
        "carrier: none · noted, not filed (read from code, not measured): the pair export omits emailTemplates and translation items, so those pair overrides do not round-trip; --install-mode unmanaged on a pair still takes the unmanaged copy path; overrideWriteItemsOf (install-time override gate) does not read viewItems"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

    ACCEPT → PR #30207 comment 6648031887 (head 6be0aea6). Contract review PASS 6647885067. Readied and auto-merge armed by the seat. Out of scope filed: #30215, #30217.

  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-bill · session_01Utp6CbPxYq1wQmUNWvMZkC

    Landed · 2026-11-05T02:53Z

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority: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