Skip to content

runtime: uninstalling a package (DELETE /packages/:id) withdraws its extension fields with no scan, so another package's active validation rule that reads one breaks every write (#30379 follow-up) #30999

Open

Description

@objectstack-fleet

domain:server#4 · os-elon-musk · session_016WV4czvZykUqhYxYXSSDt4

Filing gate: ① a named location, from card #30379 (PR #30825, merged 1de7def003). The seat named this follow-up in its ACCEPT and in the landing record 6689521847. ⛔ Not a claim.

维护者速读

停用包时,现在会先查有没有别的包的生效校验规则还在读它加的字段,有就拒绝(#30825)。卸载包(DELETE /packages/:id)同样会撤掉这些字段,却没有这一步检查:卸载后,那条规则会让该对象的所有写入失败。这张卡让卸载门复用同一个检查。

Today

Expected

  • The uninstall door refuses on the same verdict, with the same code and sentence (package · object · rule), before any state moves. Pins: an uninstall is refused while a dependent active rule reads a withdrawn field; CONTROL: the uninstall proceeds once the rule is switched off through the tenant overlay; CONTROL: a package no rule depends on uninstalls as before.
  • This is a narrowing (BREAKING): contract review, and the maintainer's per-PR word.

Suggested grade: p2 · S · domain:server.

Activity

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 6, 2026
  2. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p2 kept · domain:server · 2026-11-06T16:55Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 7, 2026
  4. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · marchtian · session_017ihL2dLQEXWGsE2SZ3Qn5F

    Claim: PM loop round 3
    Session: session_017ihL2dLQEXWGsE2SZ3Qn5F
    Account: marchtian
    Branch: claude/project-thread-of4yrh
    Worktree: card thread container (project thread form)
    Domain: domain:server
    Seat: domain:server#4
    File surface: packages/runtime/src/domains/packages.ts (the DELETE /packages/:id arm only), its tests, .changeset/; packages/metadata-protocol/src/protocol-package-uninstall.ts only if the refusal must sit in the verb — then stop and report before widening (stop on breach; explain in the report)
    Container & model: M, mode:thread (a project card thread started by the coordinator), model: default, effort: high
    Clause-②: yes
    Metadata-protocol: none
    Responsibility: objectstack (packages/runtime DELETE door) | the disable door refuses on the dependent-rule verdict (#30379 / PR #30825); the uninstall door withdraws the same objectExtensions fields without that scan, so a dependent active rule then fails every write on its object | reached by every Studio uninstall and every DELETE /api/v1/packages/:id caller
    Thread-read: 6690424333
    Premise: holds — re-measured at branch start by the dev; #31023 (PR #31183, merged 4c318214) landed the DELETE arm's actor and the verb-level disable-record clear this card builds on
    Serial constraints cleared: #31023 merged (same door); #31067 merged (the exported verdict); no other open PR touches packages/runtime/src/domains/packages.ts's DELETE arm (lane-3 PRs #31192/#31195 touch packages/cli / a runtime test only)
    Narrowing: BREAKING — contract review by the seat, and the maintainer's per-PR word before merge (triage 6690424333)

  5. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · marchtian · session_017ihL2dtwL9RzJUEskoEex5

    os-dev-report

    {
      "issue": 30999,
      "status": "done",
      "branch": "claude/project-thread-of4yrh",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/31227",
      "head": "0fc63531b2b59de8a20fbeca8c7ca9aa9bcac3c2",
      "session": "session_017ihL2dtwL9RzJUEskoEex5",
      "premise_still_valid": true,
      "summary": "DELETE /packages/:id now asks the disable door's verdict (readDisableDependentRules, the function exported as readPackageDisableDependents; no second copy) right after the read-only check and before resolveProtocol, deletePackage, the registry withdrawal and the disable-record clear, and refuses with 409 PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE and the disableDependentRulesMessage sentence under the lead \"Package 'ID' cannot be uninstalled\". The code is reused (ledgered once under @objectstack/runtime); nothing under packages/spec moves. Re-measured at branch start with the check removed: the uninstall answered 200, the field left the served object, and the next write answered 400 VALIDATION_FAILED (\"Validation rule 'qx_tier_known' could not be evaluated (runtime: No such key: qx_tier)\").",
      "tests": "New top-level describe in packages-disable-module-extension-fields.test.ts on a real kernel (ObjectQL on SQLite, MetadataPlugin, the real deletePackage spied on and passed through): refusal (409 + code, message equals the exported builder over the exported verdict, deletePackage never called, package and field still served, state file unchanged, write still lands); CONTROL no-dependent package (disabled first) uninstalls 200, verb handed {packageId, actor: u_admin}, record cleared; CONTROL rule switched off through the tenant overlay then uninstall 200 and writes land. vitest -t 30999: 3 passed. Ablation via scripts/ablation-replace.mjs (verdict replaced by an empty list; anchor 1 -> 0, blob 97f5c5fb5c8b -> 197d54665e7a; restored, blob == HEAD, git diff HEAD empty): refusal pin red, 'expected { status: 200, code: undefined } to deeply equal { status: 409, …(1) }'; the two CONTROLs red by cascade. Subject imported from src by relative path, so no dist leg. pnpm --filter @objectstack/runtime test: 554 files, 8930 passed, 19 skipped, VERDICT command-exit 0.",
      "gates": "dispatch-gates --commands at 0fc63531: 62 derived, all exit 0 (check-plugin-teardown-shape --self-test first exit 3, pinned fixture commit outside the shallow clone, fetched that commit, rerun exit 0; check:dual-build-cjs-loads first exit 3 PREREQUISITE NOT MET, rerun after full pnpm build exit 0). --ran: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN. pnpm --filter @objectstack/runtime typecheck :: exit 0. eslint packages/runtime --no-inline-config :: exit 0. pnpm build --concurrency=2 :: exit 0. CI check-runs: in_progress at report time.",
      "dogfood_battery": "0fc63531 · VERDICT command-exit 0 (held 27m19s) · Test Files 435 passed | 3 skipped (438) · Tests 3609 passed | 41 skipped (3650) · no failures",
      "line_budget": "3 files, +163 / -2 (165 changed lines; under the 3000 human-merge threshold)",
      "files_changed": [
        ".changeset/30999-uninstall-dependent-rule-refusal.md",
        "packages/runtime/src/domains/packages.ts",
        "packages/runtime/src/domains/packages-disable-module-extension-fields.test.ts"
      ],
      "deviations": [
        "Worktree: the thread checkout's branch was moved into a dedicated worktree (/home/user/objectstack-issue-30999) by detaching the primary checkout, since the branch is platform-named and already existed.",
        "check-plugin-teardown-shape --self-test: fetched the single pinned fixture commit 621a4876 instead of git fetch --unshallow.",
        "Lint: the root lint expression (eslint --no-inline-config) over packages/runtime, not repo-wide pnpm lint (CI's run).",
        "test:repo not run locally (CI owns it; the diff touches no repo-project test).",
        "Changeset level patch with Clause-② yes (narrowing) + Metadata-protocol none and an ADR-0087 not-required (no-migration-prescription) marker, per the level-axis rule (no protocol kind, so patch); check-changeset-no-major and check-adr-0087-registration exit 0. The claim's line read 'Clause-②: yes'; the (narrowing) arm was added as the order asked."
      ],
      "open_questions": [
        {
          "question": "Only the HTTP door refuses; the in-process uninstall verb (deletePackage in @objectstack/metadata-protocol) still withdraws a read field for an in-process caller (a host uninstall tool, install-local undo). Should the verb refuse too?",
          "options": [
            "A: keep this card to the door (as claimed); the seat files a follow-up card for the verb if an in-process caller matters",
            "B: widen now by registering the verdict as an uninstall precheck on the protocol (metadata-protocol cannot import runtime), refusing inside deletePackage before its store delete"
          ],
          "recommendation": "A, because the claim scoped the file surface to the DELETE arm and B adds a new seam on the verb that wants its own contract review"
        },
        {
          "question": "The refusal sentence after the uninstall lead still says the fields are ones 'the disable would withhold' and ends 'or keep the package enabled'. Keep it?",
          "options": [
            "A: keep the disable door's sentence verbatim (as the order asked; no exported signature changes)",
            "B: add an optional verb argument to formatPackageDisableDependents so the uninstall says 'the uninstall would withdraw' and 'keep the package installed'"
          ],
          "recommendation": "A for this PR; B only if the maintainer wants the wording, as an additive export change"
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — unmeasured inference: the verdict skips objects the package itself owns (as the disable door does), so a rule on another package's object reading one hop through a lookup INTO an object the uninstalled package owns is not judged; the uninstall of an owner is already refused when another package extends its object, and this case was not measured"
      ],
      "mcp_calls": "0",
      "api_writes": "2 — POST /repos/objectstack-ai/objectstack/dispatches (fleet-write relay pr_create, run 42402141357 -> #31227); this os-dev-report comment via post-stamped.mjs. git push is not a REST write.",
      "security_findings": "none"
    }
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:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions