Skip to content

packages: POST /packages/:id/commits/:commitId/revert ignores :id — a commit of package A reverts through package B's path, and lands a revert commit in A's history after A was uninstalled #28987

Closed

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbWJDYbbtH6dhJNoboN

立卡门: ① 产品缺陷,具名落点 + 复现(reach: 公开入口 HTTP POST /api/v1/packages/:id/commits/:commitId/revert 实测一次错误结果)
业务理由: 用脚本或 Studio 按包回滚的管理员,提交号一旦串错包,就会悄悄回滚另一个包的元数据(创建的条目被软删、编辑的被还原),而且回应 200、没有任何提示;即便那个包已卸载,也会往它的历史里再写一条 revert 提交。路径里的包 id 形同虚设,审计时也看不出是从哪个包发起的。
动手的读者: domain:server 席,packages/runtime/src/domains/packages.ts 的 commits/:commitId/revert 分支(约 L2419):读 parts[0],提交不属于该包则 404(如 COMMIT_NOT_FOUND);并比照约 L2506 的 POST /packages/:id/revert 决定要不要 requireWritablePackage。
查重: commit revert wrong package 1129 命中(噪声)、revertCommit 68、commits revert package id 593(均含已关);最近邻 #21821(revertCommit 恢复分支的类型复数问题,开)、#27868、#25278、#24705(都已关,别的门)都不是本现象。

QA-source: #28981 · platform-core.package-lifecycle-enable-disable · observed beside the clauses

What happens

The per-commit revert door takes the commit id from the path and never reads the package id beside it:

  • POST /api/v1/packages/B/commits/COMMIT_OF_A/revert answers 200 and reverts package A's items;
  • it then writes a "Revert: …" commit into A's history, even when A was already uninstalled (GET /packages/A/commits lists it).

The caller still needs manage_metadata, so this is not an authorization hole; it is the door doing something other than what its path names. On a stock boot no read-only package carries commits (GET /packages/com.example.showcase/commits → []), so whether a read-only package's commit can be reverted this way was not reachable here.

Measured at framework 7fa9873f (main, 2026-11-02), stock showcase boot from a scratch copy, as the seeded admin.

Reproduction

  1. As admin, create two writable packages A (com.qa.r5probe) and B (com.qa.r5probe2) with POST /api/v1/packages, put a draft into each and POST /api/v1/packages/ID/publish-drafts.
  2. Read A's first commit id from GET /api/v1/packages/com.qa.r5probe/commits. (A may be uninstalled at this point.)
  3. POST /api/v1/packages/com.qa.r5probe2/commits/A_COMMIT_ID/revert with {}.
  4. Expected: 404 naming the commit as not in package B, or a refusal naming the mismatch.
  5. Actual: 200 {revertedCount: 3, reverted: [r5p_app removed, r5p_widget.all_widgets removed, r5p_widget removed]}; GET /packages/com.qa.r5probe/commits now holds 4 entries, the newest "Revert: first publish".

Mechanism (read at 7fa9873)

The branch passes only commitId (and the actor) to protocol.revertCommit; parts[0] is unused. The sibling POST /packages/:id/revert resolves the package and calls requireWritablePackage.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:server · area:devpath · pm:queue · 2026-11-02T06:02Z

    ⛔ Not a claim.

    • Read fresh. POST /packages/:id/commits/:commitId/revert never reads :id. A commit of package A reverts through package B's path with 200, and writes a revert commit into A's history even after A was uninstalled. The caller still needs manage_metadata, so this is not an authorization hole; the door does something other than what its path names. Named location: the commits/:commitId/revert branch of packages/runtime/src/domains/packages.ts. domain:server, p2.

    Generated by Claude Code

  2. added
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 2, 2026
  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

    Claim: PM loop round 4 · card thread · 2026-11-02T06:17Z
    Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5nWnKw7n4za79sxnfc
    Account: os-project-manager (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-ae077b (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: the maintainer, 2026-11-01T03:46Z: 「server 车道现在就是你负责,任务有一大堆啊,都可以派,包括 p3」, with 2026-10-26T10:29Z 「p1 p2 处理完了,就可以处理 p3」. With 2026-11-01T12:11Z 「定级你可以自己决定」 (triage graded this card p2) and 2026-11-02T05:15Z 「队列进了很多任务,抓紧派发」; seat 1 confirmed seat 2 takes this card. Seat's route: the path's package decides. The per-commit revert (and POST /packages/:id/rollback, if it reproduces) acts only on a commit of package :id; otherwise it answers 404 and writes nothing. A read-only package is refused as the package-level revert refuses it.
    File surface: packages/runtime/src/domains/packages.ts commit revert and rollback branches; the metadata-protocol commit functions if the check lives there; their tests; one dogfood pin; changeset
    Container & model: M, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: medium
    Clause-②: yes (narrowing) expected: calls answered 200 are now refused; the dev measures and declares; CONTRACT_REVIEW_TIER review
    Metadata-protocol: none (expected; ruling A #25105 ships a non-protocol narrowing at patch with the BREAKING banner; the dev measures and declares)
    Responsibility: the dev adds one package check for both doors, red first with ablation; the seat reviews and lands
    Thread-read: none
    Premise: measured by the QA author on main and confirmed by triage (6570304645)
    Serial constraints cleared: none; overlap re-read of open PRs on packages.ts and the protocol commit functions before pr_create
    Resume-from: none — fresh from origin/main

    State: → pm:dispatched, assigned to os-project-manager, recorded by this claim.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-project-manager · session_01FEkQ5nWnKw7n4za79sxnfc

    os-dev-report
    {
    "issue": 28987,
    "status": "done",
    "branch": "claude/project-thread-ae077b",
    "pr": "#29090",
    "session": "session_01FEkQ5nWnKw7n4za79sxnfc",
    "premise_still_valid": true,
    "summary": "POST /packages/:id/commits/:commitId/revert and POST /packages/:id/rollback now hand the path's package to the protocol as packageId; readRevertableCommit refuses a commit of another package with the existing COMMIT_NOT_FOUND 404 ('No commit ID in package PKG.') before anything is written, and an unknown :id answers the sibling doors' 404 Package 'ID' not found (inside each door's capability probe, after the 400 body check on rollback). Measured: read-only packages carry commits (environment overlay publishes via publish-drafts), so per the seat's option A no requireWritablePackage was added; the overlay commit revert is pinned as a control. Work was paused on the maintainer's word after the PR opened; every verification run here finished on the same head.",
    "tests": "Red on main ab085a0 (dogfood file, dist from main): 3 failed | 4 passed (7) — cross-package revert 200 revertedCount 1, cross-package rollback 200, unknown package 200. Green on head a4647aa: dogfood file 7 passed (7); pnpm --filter @objectstack/runtime test 514 passed files, 8273 passed | 19 skipped tests; objectql protocol-commit-history.test.ts 32 passed (32). Ablation via scripts/ablation-replace.mjs on readRevertableCommit (condition never matches), metadata-protocol rebuilt, ablation-dist-preflight marker present in 2 built files: dogfood 3 failed | 4 passed (the cross-package revert, rollback and the overlay case's cross-package step only), objectql 2 failed | 30 passed (the two new mismatch cases only); restored blob == HEAD, git diff HEAD empty, rebuilt, preflight --absent clean. A first ablation with if (false && ...) was folded away by the bundler (preflight: marker ABSENT) and its reading discarded. Typecheck (metadata-protocol, runtime and objectql with check:test-typecheck, dogfood): exit 0.",
    "gates": "Local: dispatch-gates --commands derived 67; --ran reconciliation: 66 run exit 0, 1 NOT-MEASURED (check:dual-build-cjs-loads exit 3, prerequisite every package dist not met). check-plugin-teardown-shape --self-test exit 3 on the shallow clone, exit 0 (48 cases) after git fetch --depth=1 of its fixture commit. check-adr-0087-registration --base origin/main exit 0, not-required (no-migration-prescription). Record: pm-orders/server/28987-gates-ran.txt. CI on a4647aa at report time: 48 check-runs, 31 success, 13 skipped, 4 in progress (Test Core 3/8 to 6/8), none failed.",
    "dogfood_battery": "a4647aa1 · os-verify-lock: VERDICT command-exit 0 · Test Files 384 passed | 3 skipped (387) · Tests 3170 passed | 28 skipped (3198) · no failures",
    "mcp_calls": "0",
    "api_writes": "2 — fleet-write pr_create (repos/objectstack-ai/objectstack/pulls, #29090, read back identical, no assignees); this os-dev-report comment (issues/28987/comments) via post-stamped",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed — an uninstalled package's commit is now refused through its own path too (the ruling's unknown-package 404); GET /packages/:id/commits still serves the trail. Stated in the PR's Acceptance notes."
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

    Release: session_0186vfjHHMsRXC9EUT9hTFMo · cause: the maintainer's wrap-up (「进展中的开发先提交代码然后暂停,先确保已有的pr都合并」, 「草稿的也交给下一任,在座位帖写清楚」); seat 2 signs off · next: Draft PR #29090, implementation and verification complete (os-dev-report 6571600286). Next: patch the body Verification from pm-orders/server/28987-pr-body-addendum.md, run a CONTRACT_REVIEW_TIER review (Clause-②: yes (narrowing), optional packageId on the protocol), then ACCEPT and land. Seat ruling: no requireWritablePackage on the commit doors. Re-claim on the same branch.
    Resume-from: claude/project-thread-ae077b@a4647aa192a5f76356d05441074559b0a324387e

    State: pm:dispatched → pm:queue, assignee cleared, in this same relay stroke. Seat hand-off: mailbox #28284 comment 6571506250; seat post #20966.


    Generated by Claude Code

  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

    Claim: PM loop round 1 · seat takeover · 2026-11-02T08:14Z
    Session: session_01ADHkmQxwoGb5fSnrFkpxmt (the seat)
    Account: os-steve (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-ae077b (continued from the released claim; PR #29090)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: the maintainer, verbatim: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」 (os-steve project "objectstack spec"). Takes over the hand-back Release: 6571758819 of seat 2's previous term (session_0186vfjHHMsRXC9EUT9hTFMo).
    File surface: packages/runtime/src/domains/packages.ts commit revert and rollback branches; the metadata-protocol commit functions; their tests; one dogfood pin; changeset (unchanged from claim 6570512674)
    Container & model: M, mode:thread, model: opus (as the previous claim), effort: medium
    Clause-②: yes (narrowing) — as declared on draft PR #29090; CONTRACT_REVIEW_TIER review owed before landing
    Metadata-protocol: none (expected; as declared on the PR)
    Responsibility: the dev added one package check for both doors (claim 6570512674); os-dev-report 6571600286 is on the card. The seat patches the PR body's Verification, obtains the contract review, ACCEPTs and lands. Seat ruling carried: no requireWritablePackage on the commit doors
    Thread-read: 6571758819
    Premise: holds (measured by the previous term's dev; re-read on the PR head before ACCEPT)
    Serial constraints cleared: none new; the PR's file list is re-read against live claims before landing
    Resume-from: claude/project-thread-ae077b@a4647aa192a5f76356d05441074559b0a324387e

    Status: draft PR #29090, implementation and verification complete; the seat resumes it.

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

    ACCEPT → PR #29090 comment 6572440163 (landing waits on a same-head contract review PASS)

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain: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