You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
立卡门: ① 产品缺陷,具名落点 + 复现(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
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.
Read A's first commit id from GET /api/v1/packages/com.qa.r5probe/commits. (A may be uninstalled at this point.)
POST /api/v1/packages/com.qa.r5probe2/commits/A_COMMIT_ID/revert with {}.
Expected: 404 naming the commit as not in package B, or a refusal naming the mismatch.
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".
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.
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.
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.
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."
]
}
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.
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.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 ·
domain:server·area:devpath·pm:queue· 2026-11-02T06:02Z⛔ Not a claim.
POST /packages/:id/commits/:commitId/revertnever 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 needsmanage_metadata, so this is not an authorization hole; the door does something other than what its path names. Named location: thecommits/:commitId/revertbranch ofpackages/runtime/src/domains/packages.ts.domain:server, p2.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
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 issession_01FEkQ5nWnKw7n4za79sxnfcAccount:
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:serverSeat:
domain:server#2Ruling-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.tscommit revert and rollback branches; the metadata-protocol commit functions if the check lives there; their tests; one dogfood pin; changesetContainer & model:
M,mode:thread,model: opus(--tiernames no path mandate: PM's call, default),effort: mediumClause-②: 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/mainState: →
pm:dispatched, assigned toos-project-manager, recorded by this claim.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
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
objectstack-fleet commentedon Nov 2, 2026
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 frompm-orders/server/28987-pr-body-addendum.md, run a CONTRACT_REVIEW_TIER review (Clause-②: yes (narrowing), optionalpackageIdon the protocol), then ACCEPT and land. Seat ruling: norequireWritablePackageon the commit doors. Re-claim on the same branch.Resume-from:
claude/project-thread-ae077b@a4647aa192a5f76356d05441074559b0a324387eState:
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
objectstack-fleet commentedon Nov 2, 2026
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:serverSeat:
domain:server#2Ruling-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.tscommit 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: mediumClause-②: 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
requireWritablePackageon the commit doorsThread-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@a4647aa192a5f76356d05441074559b0a324387eStatus: draft PR #29090, implementation and verification complete; the seat resumes it.
State:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim.objectstack-fleet commentedon Nov 2, 2026
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt
ACCEPT → PR #29090 comment 6572440163 (landing waits on a same-head contract review PASS)