Skip to content

packages: the in-process uninstall verb (deletePackage) withdraws extension fields a dependent active validation rule reads, with no refusal — only the HTTP door asks #31232

Closed

Description

@objectstack-fleet

domain:server#4 · marchtian · session_017ihL2dLQEXWGsE2SZ3Qn5F

Filing gate: ① product defect (the same break #30999 closes at the HTTP door, left open on the in-process path), from the delivery of #30999 (PR #31227, dev report 6711617406, decision 1). ⛔ Not a claim. Triage sets the grade and the lane.
Reader: the seat that owns packages/metadata-protocol (deletePackage, protocol-package-uninstall.ts) and packages/runtime/src/domains/packages.ts.
Dedupe (open AND closed): deletePackage dependent rule in-process → see the search on this filing; the related cards are #30379 (disable door), #30999 (HTTP uninstall door), #31023 (verb-level disable-record clear).
reach: every in-process deletePackage caller — cloud's uninstall AI tool (cloud#3656), install-local's DELETE and pair-install undo, the unmanaged-import rollback; read from the code, not run.
class: c · type: Bug

维护者速读

#30999 让 Studio / HTTP 卸载在「别的包有生效校验规则还在读它加的字段」时直接拒绝。但平台内部直接调用卸载(cloud 的 AI 工具、本地安装、导入回滚)不走这个入口,仍然会把字段撤掉,之后那条规则会让该对象的所有写入失败。

What is wrong

Expected

Activity

  1. added
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    domain:serverDomain lane — seat card indexed by label:pm:seat
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Nov 7, 2026
  2. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p2 · domain:server · 2026-11-07T19: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 4
    Session: session_017ihL2dLQEXWGsE2SZ3Qn5F
    Account: marchtian
    Branch: (the card thread's, posted in the order)
    Worktree: card thread container (project thread form)
    Domain: domain:server
    Seat: domain:server#4
    File surface: packages/metadata-protocol/src/** (the uninstall verb deletePackage / protocol-package-uninstall.ts: one pre-uninstall guard seam beside registerUninstallCleanup), packages/runtime/src/** (registering the existing dependent-rule verdict as that guard; the HTTP DELETE arm only to read the same guard), their tests, .changeset/ (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 (narrowing)
    Metadata-protocol: none
    Responsibility: objectstack (package uninstall verb) | after PR #31227 the HTTP door refuses an uninstall that would withdraw a field an active validation rule reads; protocol.deletePackage does not ask, so in-process callers withdraw it and the rule then fails every write to its object | reached by cloud#3656's uninstall tool, install-local DELETE, pair-install undo, unmanaged-import rollback
    Thread-read: 6712270326
    Premise: holds — read from code (no verdict read in deletePackage); re-measured by the dev at branch start
    Design: seat ruling — the runtime registers the ONE verdict (readDisableDependentRules) on the protocol as a pre-uninstall guard, the mirror of #31023's registerUninstallCleanup; metadata-protocol never imports runtime and never holds a copy of the rule
    Serial constraints: PR #31227 (#30999, open, BREAKING, awaiting the maintainer) owns the HTTP DELETE arm's refusal in packages/runtime/src/domains/packages.ts; this card builds on top of it — the dev merges origin/main once #31227 lands and the PR does not leave draft before then. #31186 (claimed, plugin-audit writer) may register a post-uninstall cleanup on the same verb; no shared lines expected. BREAKING: contract review + the maintainer's per-PR word before merge.

  5. objectstack-fleet commented on Nov 7, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · marchtian · session_017ihL2c1rk1PGZ51s32cxZt

    os-dev-report

    {
      "issue": 31232,
      "status": "done",
      "branch": "claude/project-thread-qrn7nl",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/31280",
      "head": "e052b1927",
      "session": "session_017ihL2c1rk1PGZ51s32cxZt",
      "premise_still_valid": true,
      "summary": "Seat ruling implemented: ObjectStackProtocolImplementation#registerUninstallGuard(name, guard) is the before-half of registerUninstallCleanup; deletePackage asks every registered guard in registration order right after the retired-key refusal and before its first read, and throws the first refusal (message, code, status as the guard spelled them); a guard's throw propagates; no guard registered = the old path. The runtime registers one guard, runtime.package-dependent-rules (ensureDependentRulesUninstallGuard, from AppPlugin.start beside ensurePackageStateUninstallCleanup), answering packageUninstallDependentRulesRefusal: readDisableDependentRules + disableDependentRulesMessage under the uninstall lead, 409 PACKAGE_DISABLE_RESTRICTED_BY_VALIDATION_RULE. metadata-protocol imports nothing from runtime. No packages/spec change, no new error code. Built against main's two-argument builder because #31227 is still open; the HTTP DELETE arm is untouched.",
      "tests": "metadata-protocol: new protocol.uninstall-guards.test.ts (6 cases: refusal with nothing read or moved, actor + first-refusal-wins, guard throw, replace-by-name, CONTROL guards pass, CONTROL bare protocol). runtime: new top-level describe [#31232] in packages-disable-module-extension-fields.test.ts (4 cases on a real kernel: in-process refusal with findEnvironmentRows/deleteMetaItem/runUninstallCleanups/registry.uninstallPackage never called, package+field still served, state file unchanged, write lands; DELETE door answers the same message; CONTROL no dependent uninstalls in process and clears the disable record; CONTROL rule switched off via tenant overlay). Premise re-measure (guard call removed): deletePackage answered without throwing, the package and field were withdrawn, next write 400 VALIDATION_FAILED 'No such key: qz_tier'. Ablation via ablation-replace.mjs on the guard call in protocol-package-uninstall.ts, metadata-protocol rebuilt, dist-preflight marker present in dist/index.js + dist/index.cjs: guard file 5 red / 1 green (bare-protocol control), [#31232] refusal red ('the in-process uninstall was not refused'), door case red by cascade (404), controls green; restored blob == HEAD 24b1fe27dc2a, rebuilt, preflight --absent clean (24 files), tree clean. First ablation spelling used a comment marker no build emits; restored and redone with a code marker. Full suites: metadata-protocol 323 files / 22533 passed / 26 skipped; runtime 555 files / 8943 passed / 19 skipped; typecheck both exit 0.",
      "gates": "At head e052b1927 (after merging origin/main): dispatch-gates --commands derived 67; all 67 :: exit 0. check:dual-build-cjs-loads first exit 3 (no full build) and passed after pnpm build; check:test-source-alias first exit 1 on the new metadata-protocol test, fixed with a module-top import of @objectstack/lint as the gate prescribes, then exit 0. --ran: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN. eslint packages/metadata-protocol packages/runtime --no-inline-config :: exit 0. PR check-runs: in_progress at report time (not waited on).",
      "dogfood_battery": "e052b1927 · os-verify-lock VERDICT command-exit 0 (1645s) · Test Files 435 passed, 3 skipped (438) · Tests 3610 passed, 41 skipped (3651) · no failures",
      "line_budget": "11 files, +499 / -7 against the merge-base: src 194 (metadata-protocol 120, runtime 74), tests 292, changeset 20",
      "files_changed": [
        ".changeset/31232-uninstall-verb-dependent-rule-guard.md",
        "packages/metadata-protocol/src/index.ts",
        "packages/metadata-protocol/src/protocol-class-head.ts",
        "packages/metadata-protocol/src/protocol-host.ts",
        "packages/metadata-protocol/src/protocol-package-lifecycle-types.ts",
        "packages/metadata-protocol/src/protocol-package-uninstall.ts",
        "packages/metadata-protocol/src/protocol.ts",
        "packages/metadata-protocol/src/protocol.uninstall-guards.test.ts",
        "packages/runtime/src/app-plugin.ts",
        "packages/runtime/src/domains/packages-disable-module-extension-fields.test.ts",
        "packages/runtime/src/domains/packages.ts"
      ],
      "deviations": [
        "Built against main's two-argument formatPackageDisableDependents: #31227 is open, so the guard's sentence currently says 'which the disable would withhold ... or keep the package enabled'. After #31227 lands: merge main, pass { operation: 'uninstall' } in packageUninstallDependentRulesRefusal; the pins compare against the builder and need no new literal.",
        "HTTP DELETE arm not edited (#31227 open). On today's main the door has no check of its own, so on this branch the door refuses through the verb's guard (its catch relays the thrown 409 + code via errorFromThrown); pinned by the door case.",
        "The worktree was created from the card-thread clone for the platform-assigned branch (no empty-branch probe push, per os-dev rule 1)."
      ],
      "mcp_calls": "0",
      "api_writes": "2 — relay pr_create (fleet-write run 42418514877, POST /repos/objectstack-ai/objectstack/pulls, draft, assignees marchtian) and this comment via post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/31232/comments)",
      "open_questions": [
        {
          "question": "After #31227 lands, which single check refuses at the HTTP DELETE arm?",
          "options": [
            "A: keep the door's own check (it runs first), changed to call packageUninstallDependentRulesRefusal so the door and the guard spell one function's sentence; the guard covers every in-process caller",
            "B: delete the door's check and rely on the verb's guard; the door's catch relays the 409"
          ],
          "recommendation": "A, because the door also serves a host whose protocol has no deletePackage (registry-only uninstall) or no guard registered (no AppPlugin), where B would lose the refusal; with one function the two can never answer different words, and on the door path the guard is simply never reached."
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: a · reach: DELETE /api/v1/marketplace/install-local/:manifestId (@objectstack/cloud-connection) — read from the code, not run: it withdraws through registry.uninstallPackage and runs runUninstallCleanups itself, never calling deletePackage, so neither the new guard nor readDisableDependentRules is asked; a package whose objectExtensions field another enabled package's active rule reads is withdrawn and that rule then fails every write to its object · evidence: marketplace-install-local-plugin.ts asks assertPackageUninstallable before its ledger write and has no dependent-rule check (grep readPackageDisableDependents / PACKAGE_DISABLE_RESTRICTED: 0 hits) · suggested shape: a public runner of the guard registry on the protocol (the mirror of runUninstallCleanups) that the door asks before its ledger write · dedupe words: install-local uninstall dependent rule, marketplace install-local DELETE objectExtensions, runUninstallGuards"
      ]
    }
  6. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · marchtian · session_017ihL2c1rk1PGZ51s32cxZt

    os-dev-report

    {
      "issue": 31232,
      "status": "done",
      "round": "rework 1 (after #31227 landed as 57675a21)",
      "branch": "claude/project-thread-qrn7nl",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/31280",
      "head": "ec3a1429c",
      "session": "session_017ihL2c1rk1PGZ51s32cxZt",
      "premise_still_valid": true,
      "summary": "Merged origin/main (57675a21, #31227) as fffe4d622: the only conflict was the shared harness file, where both PRs appended a describe; resolved as main's file plus this PR's [#31232] describe and imports. d17be3272: the DELETE arm now asks packageUninstallDependentRulesRefusal (no inline scan) and answers its code/status/message, so the door and the verb's guard (runtime.package-dependent-rules) answer through one function; the door's check stays ahead of resolveProtocol (seat ruling A) because a host with no deletePackage or no guard uninstalls through the registry. The guard's builder call now passes { operation: 'uninstall' }, and the in-process refusal pin holds the uninstall sentence literally ('which the uninstall would withdraw ... or keep the package installed.'). ec3a1429c: changeset names the uninstall sentence and the shared door refusal. PR stays draft.",
      "tests": "Harness packages-disable-module-extension-fields.test.ts + package-state-host-store.test.ts: 56/56 (incl. #31227's 4 and this PR's 4). metadata-protocol guard/cleanup/delete-refusal files: 21/21. Full: runtime 555 files, 8953 passed, 19 skipped; metadata-protocol 325 files passed (4 skipped), 22542 passed, 26 skipped. Ablation on the door line (packageUninstallDependentRulesRefusal call -> undefined, src import, no dist leg): #31227's refusal pin red only on 'expected deletePackage to not be called at all, but actually been called 1 times' (status, code and message still matched: the verb's guard answered the same refusal), its control red by cascade; restored blob == HEAD f1aa27176c2f, tree clean.",
      "gates": "At head ec3a1429c: dispatch-gates --commands derived 73; all 73 :: exit 0; --ran: 73 derived, 73 run, 0 NOT-MEASURED, 0 UNRUN. runtime + metadata-protocol typecheck exit 0. eslint packages/metadata-protocol packages/runtime --no-inline-config :: exit 0.",
      "dogfood_battery": "NOT MEASURED at ec3a1429c, reason: declared narrowing for this rework (the delta is the door's call site and the builder argument, pinned by the runtime harness); the last full run was green at e052b1927 (435 files / 3610 tests). CI's queue run carries the dogfood shards.",
      "deviations": [
        "Dogfood not rerun on the rework head (declared above).",
        "The merge commit carries only the conflict resolution; the door/guard change is its own commit d17be3272."
      ],
      "mcp_calls": "0",
      "api_writes": "1 — this comment via post-stamped.mjs",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: #31283 (install-local DELETE asks no guard) · noted, not touched"
      ]
    }
  7. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · marchtian · session_017ihL2c1rk1PGZ51s32cxZt

    os-dev-report

    {
      "issue": 31232,
      "status": "done",
      "round": "union merge after #31286 landed (8ef25237)",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/31280",
      "head": "1cc4d5f3a",
      "session": "session_017ihL2c1rk1PGZ51s32cxZt",
      "summary": "Merged origin/main (8ef25237, #31286) as 1cc4d5f3a; no other change. Conflicts resolved as the union: deletePackage's this-Pick lists environmentId and uninstallGuards; the protocol.ts host literal reads both (readEnvironmentId, readUninstallGuards); askUninstallGuards stays right after the retired-key refusal and before the first read, and #31286's recordPackageUninstall stays at the end; both helper blocks are kept at the foot of protocol-package-uninstall.ts (askUninstallGuards, then the audit event type, readInstalledPackage and recordPackageUninstall).",
      "tests": "metadata-protocol protocol.uninstall-guards, uninstall-cleanups-runner, package-delete-refusal (incl. #31286's audit cases), uninstall-declared-capture: 4 files, 36/36. plugin-audit package-lifecycle-audit.test.ts: 9/9. runtime packages-disable-module-extension-fields, package-uninstall-store-refusal.integration, package-state-host-store: 3 files, 67/67. metadata-protocol + runtime typecheck exit 0; eslint on the two resolved files exit 0; pnpm check:durability-log-level exit 0.",
      "gates": "Scoped to the merge per the seat's order; the full derived set and dogfood are not rerun (NOT MEASURED, reason: merge-only round; CI runs them on this head).",
      "mcp_calls": "0",
      "api_writes": "1 — this comment via post-stamped.mjs",
      "security_findings": "none"
    }
  8. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#4 · marchtian · session_017ihL2dLQEXWGsE2SZ3Qn5F

    Landed · PR #31280 merged by the merge queue as 4363945552ce00bbdfc87cd0f3fe3a751378633c. The maintainer approved this BREAKING narrowing in the card thread, and the seat queued it.

    Reading Value
    PR state merged at 2026-11-08T03:34Z, merged_at from the pulls API
    Card state closed completed by Fixes #31232
    origin/main content ObjectStackProtocolImplementation#registerUninstallGuard exists, and deletePackage asks the guards before its first read. AppPlugin registers runtime.package-dependent-rules through ensureDependentRulesUninstallGuard. DELETE /packages/:id and the guard share packageUninstallDependentRulesRefusal

    Follow-up: #31283 (install-local's own DELETE door bypasses the verb, so it asks neither the guard nor the audit). pm:dispatched removed with this record.

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:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions