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
Approvals: GET /api/v1/approvals/requests?status=VALUE answers 200 with an empty list for a status that does not exist, instead of refusing it #29880
立卡门: ① 产品缺陷,具名落点 + 复现(reach: 公开入口 HTTP GET /api/v1/approvals/requests?status=bogus 实测一次错误结果,复测一致)
业务理由: 对接审批收件箱的集成(机器人、提醒脚本、第三方面板)把状态值拼错(Pending、waiting)时,拿到的是「没有待审」而不是错误,于是该提醒的审批被静默跳过。同一扇门对未知的参数名已经给出定位 400,对未知的取值却静默放行,是 #3948 家族的静默窄化。
动手的读者: domain:server 席,packages/rest/src/approvals-routes.ts 列表路由(~L262 只校验参数名,~L303 原样转发 q.status),按 sys_approval_request.status 的取值集校验并 400 指名允许值,附 HTTP 层 pin。
查重: repo is:issue approvals requests status 271 命中、approval list unknown status value 369(均含已关,前几页为 qa-run 记录与无关卡);最近邻 #27451(未知 request id 的 /actions 答 200 [],已关)不是本现象。
QA-source: #29877 · approvals.account-app-entry · observed beside the clauses
What happens
The approvals request list refuses an unknown query parameter NAME with a located 400, but accepts any status VALUE: GET /api/v1/approvals/requests?status=bogus answers 200 {"data":[]}. A caller cannot tell "nothing in that state" from "that state does not exist".
Measured at framework 589049b0 (main, 2026-11-03), stock showcase, as a non-admin approver and as admin, twice.
Reproduction
Boot the stock showcase; sign in as any member who has pending approvals (or as admin).
GET /api/v1/approvals/requests?status=pending → 200 with the pending rows (control).
GET /api/v1/approvals/requests?status=bogus (or Pending).
Expected: 400 naming status and its allowed values, as the door already does for an unknown parameter name.
packages/rest/src/approvals-routes.ts: the list handler checks parameter names against an allow-list (~L262) and passes status: q.status (~L303) to the service unvalidated; the service filters on it and finds nothing.
Claim: PM loop round 1
Session: session_01Utp6CZMkHvuDeVYgTD73qF (seat) · dev card thread session_01Utp6CbAccy76jpUtQ9ZZX9
Account: os-bill
Branch: claude/project-thread-kjhqfe (the card thread's designated push branch, the recorded platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-29880
Domain: domain:server
Seat: domain:server#4
File surface: packages/rest/src/approvals-routes.ts (the GET /api/v1/approvals/requests list route), its tests, one changeset. ⛔ packages/spec/**, docs/adr/**, .claude/**, repo-root AGENTS.md, ui/**, skills/** (stop on breach; explain in the report)
Container & model: S, mode:thread, model: default judgment tier (opus) — dispatch-gates --tier: no path-derived mandate, effort: default
Clause-②: yes (narrowing) — an unknown status value answered 200 [] and will answer 400 naming the allowed values; known values unchanged
Metadata-protocol: none
Responsibility: the approvals list route checks query parameter names but forwards q.status unchecked | none: GET /api/v1/approvals/requests?status=bogus answers 200 {"data":[]} (QA run #29877) | any integration polling the approvals inbox with a misspelled status, which silently skips pending approvals
Thread-read: 6600556144
Premise: holds (card measured at 589049b0; the dev re-measures first)
Serial constraints cleared: seats 2, 3 and 5 confirmed no overlap; no card in flight edits approvals-routes.ts.
Dispatch order: /mnt/project-files/server-seat4/dispatch-29880.md (project shared folder). Ruling section: the card and triage first-touch 6600556144 (p3, domain:server), dispatched on the maintainer's 「并发都到5」. BREAKING (!): a contract review and the maintainer's word come before landing. Fixes #29880.
Dev card thread session_01Utp6CbAccy76jpUtQ9ZZX9, branch claude/project-thread-kjhqfe, Claim 6657794401. Draft PR #30491 (author objectstack-fleet[bot]), head 976da77f.
{
"issue": 29880,
"status": "done",
"branch": "claude/project-thread-kjhqfe",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30491",
"session": "session_01Utp6CbAccy76jpUtQ9ZZX9",
"premise_still_valid": true,
"summary": "Re-measured at c5800818 on the route's own handler with a recording service: ?status=bogus and ?status=Pending answered 200 {data:[]} with the string forwarded as is; ?status=pending,returned (what @objectstack/client approvals.listRequests sends for an array) was forwarded as ONE string and also answered 200 []; a repeated status was already refused 400 by the arity gate. Fix: module-private readApprovalStatusFilter in approvals-routes.ts splits on commas, trims, requires each entry to be one of APPROVAL_STATUSES (@objectstack/spec/contracts, from which sys_approval_request.status's options are built, #3786), and otherwise answers 400 with the nested VALIDATION_ERROR body the name and arity gates on the same handler use, naming each bad value once and every allowed value; the service is not called. Empty value / empty entry refused (today ?status= answers every status). One status reaches the service as a string, several as a deduped ApprovalStatus[] (service pushes $in); countRequests gets the same filter. Runs after the name and arity gates. Known status with no rows still 200 []. Seat agreed to the comma split, the contract source and the empty refusal before the gates ran.",
"tests": "New packages/rest/src/rest-server-approvals-status-value.test.ts, 16 cases on the #7527 harness (real RestServer, registered handler, recording approvals service). Refusals assert 400, nested error.code, the located message, and that listRequests and countRequests never ran: bogus, Pending, a comma list with one bad entry named once, '', 'pending,', a nested query object; the name and arity gates still answer first. Preservation asserts the argument the service received: all six statuses as strings, known status with no rows 200 [], comma list as array, trim + dedupe, absent status leaves the filter unset, paged comma list counts total on the same array. Updated rest-server-approvals-unknown-filter.test.ts: the every-name case now sends status=pending instead of '1'. Ablation via scripts/ablation-replace.mjs replacing the readApprovalStatusFilter call with a pass-through (blob a9c203f5 -> 6b2eac2a): 8 failed / 8 passed (every refusal and the three comma-list preservation cases fail; single-status and gate-order cases hold, as they must); restored, git diff HEAD empty. @objectstack/rest vitest: 392 files, 7639 passed / 352 skipped. pnpm --filter @objectstack/rest run typecheck (tsc + check:test-typecheck): exit 0.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at 976da77f: 58 commands, each recorded command :: exit code to a file, after a full turbo run build --filter='!@objectstack/docs' (exit 0) and a fetch of 621a4876 for the teardown self-test. All 58 exit 0. --ran: 58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN. pnpm lint (repo-wide): exit 0. PR check-runs at 976da77f: 35 success, 13 skipped, none pending or failed.",
"dogfood_battery": "976da77f · os-verify-lock VERDICT command-exit 0 (held 29m23s) · pnpm --filter @objectstack/dogfood test · Test Files 419 passed / 3 skipped (422) · Tests 3492 passed / 36 skipped (3528) · no failures",
"line_budget": "296 changed lines (+294 / -2) across 4 files, under the 3000 human-merge threshold",
"files_changed": [
"packages/rest/src/approvals-routes.ts",
"packages/rest/src/rest-server-approvals-status-value.test.ts",
"packages/rest/src/rest-server-approvals-unknown-filter.test.ts",
".changeset/29880-approvals-list-unknown-status.md"
],
"deviations": [
"Allowed set read from APPROVAL_STATUSES (the contract constant the object's select options are built from), not by a runtime metadata read of sys_approval_request: same single source, no per-poll metadata read and no failure arm. Seat agreed 12:40Z.",
"Comma list now reaches the service as an array (a widening half beside the narrowing); without it the SDK's array call would turn from silent [] into 400. Seat agreed 12:40Z.",
"The full workspace build before the gates ran outside os-verify-lock (the dogfood battery ran under it).",
"main moved 18 commits after the merge (rest overlap: one unrelated test file, analytics-platform-scope-predicate-guard.test.ts). Not re-merged; head held for the contract review.",
"Reviewer request and assignee not written (order: no MCP write; relay pr_create sent no assignees)."
],
"mcp_calls": "0 — no GitHub MCP call of any kind",
"api_writes": "3 — git push (branch), relay pr_create (#30491, 7415 bytes sent = stored), this os-dev-report via post-stamped",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: none filed · carrier: PR Acceptance notes · ui/apps/console/src/pages/system/ApprovalsInboxPage.tsx status picker offers pending/approved/rejected/recalled/returned but not cancelled, which the route and the object both have (ui lane, not touched)."
]
}
MERGED 2026-11-05T15:26Z, merge commit 75bbebb27; Fixes #29880 closed this card. Seat record 6660126726 on head 976da77f (BREAKING, not governed, ≤ 3000 lines; contract review PASS 6659996154; landed per pm-dispatch on the maintainer's 13:47Z word).
Activity
objectstack-fleet commentedon Nov 3, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p3 ·
domain:server·pm:queue· 2026-11-03T09:56Z⛔ Not a claim.
GET /approvals/requests?status=<unknown>answers 200 with an empty list instead of refusing. It is a silent narrowing of the A filter with an operator outside VALID_AST_OPERATORS is silently dropped, not rejected — single-condition views return unfiltered results #3948 family on one door, so p3.Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
Claim: PM loop round 1
Session:
session_01Utp6CZMkHvuDeVYgTD73qF(seat) · dev card threadsession_01Utp6CbAccy76jpUtQ9ZZX9Account:
os-billBranch:
claude/project-thread-kjhqfe(the card thread's designated push branch, the recorded platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-29880Domain:
domain:serverSeat:
domain:server#4File surface:
packages/rest/src/approvals-routes.ts(theGET /api/v1/approvals/requestslist route), its tests, one changeset. ⛔packages/spec/**,docs/adr/**,.claude/**, repo-rootAGENTS.md,ui/**,skills/**(stop on breach; explain in the report)Container & model:
S,mode:thread,model: default judgment tier (opus) — dispatch-gates --tier: no path-derived mandate,effort: defaultClause-②: yes (narrowing) — an unknown
statusvalue answered 200[]and will answer 400 naming the allowed values; known values unchangedMetadata-protocol: none
Responsibility:
the approvals list route checks query parameter names but forwards q.status unchecked | none: GET /api/v1/approvals/requests?status=bogus answers 200 {"data":[]} (QA run #29877) | any integration polling the approvals inbox with a misspelled status, which silently skips pending approvalsThread-read: 6600556144
Premise: holds (card measured at
589049b0; the dev re-measures first)Serial constraints cleared: seats 2, 3 and 5 confirmed no overlap; no card in flight edits
approvals-routes.ts.Dispatch order:
/mnt/project-files/server-seat4/dispatch-29880.md(project shared folder). Ruling section: the card and triage first-touch 6600556144 (p3,domain:server), dispatched on the maintainer's 「并发都到5」. BREAKING (!): a contract review and the maintainer's word come before landing.Fixes #29880.Posted 2026-11-05T12:17Z.
objectstack-fleet commentedon Nov 5, 2026
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
os-dev-report
Dev card thread
session_01Utp6CbAccy76jpUtQ9ZZX9, branchclaude/project-thread-kjhqfe, Claim 6657794401. Draft PR #30491 (authorobjectstack-fleet[bot]), head976da77f.{ "issue": 29880, "status": "done", "branch": "claude/project-thread-kjhqfe", "pr": "https://github.com/objectstack-ai/objectstack/pull/30491", "session": "session_01Utp6CbAccy76jpUtQ9ZZX9", "premise_still_valid": true, "summary": "Re-measured at c5800818 on the route's own handler with a recording service: ?status=bogus and ?status=Pending answered 200 {data:[]} with the string forwarded as is; ?status=pending,returned (what @objectstack/client approvals.listRequests sends for an array) was forwarded as ONE string and also answered 200 []; a repeated status was already refused 400 by the arity gate. Fix: module-private readApprovalStatusFilter in approvals-routes.ts splits on commas, trims, requires each entry to be one of APPROVAL_STATUSES (@objectstack/spec/contracts, from which sys_approval_request.status's options are built, #3786), and otherwise answers 400 with the nested VALIDATION_ERROR body the name and arity gates on the same handler use, naming each bad value once and every allowed value; the service is not called. Empty value / empty entry refused (today ?status= answers every status). One status reaches the service as a string, several as a deduped ApprovalStatus[] (service pushes $in); countRequests gets the same filter. Runs after the name and arity gates. Known status with no rows still 200 []. Seat agreed to the comma split, the contract source and the empty refusal before the gates ran.", "tests": "New packages/rest/src/rest-server-approvals-status-value.test.ts, 16 cases on the #7527 harness (real RestServer, registered handler, recording approvals service). Refusals assert 400, nested error.code, the located message, and that listRequests and countRequests never ran: bogus, Pending, a comma list with one bad entry named once, '', 'pending,', a nested query object; the name and arity gates still answer first. Preservation asserts the argument the service received: all six statuses as strings, known status with no rows 200 [], comma list as array, trim + dedupe, absent status leaves the filter unset, paged comma list counts total on the same array. Updated rest-server-approvals-unknown-filter.test.ts: the every-name case now sends status=pending instead of '1'. Ablation via scripts/ablation-replace.mjs replacing the readApprovalStatusFilter call with a pass-through (blob a9c203f5 -> 6b2eac2a): 8 failed / 8 passed (every refusal and the three comma-list preservation cases fail; single-status and gate-order cases hold, as they must); restored, git diff HEAD empty. @objectstack/rest vitest: 392 files, 7639 passed / 352 skipped. pnpm --filter @objectstack/rest run typecheck (tsc + check:test-typecheck): exit 0.", "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack at 976da77f: 58 commands, each recorded command :: exit code to a file, after a full turbo run build --filter='!@objectstack/docs' (exit 0) and a fetch of 621a4876 for the teardown self-test. All 58 exit 0. --ran: 58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN. pnpm lint (repo-wide): exit 0. PR check-runs at 976da77f: 35 success, 13 skipped, none pending or failed.", "dogfood_battery": "976da77f · os-verify-lock VERDICT command-exit 0 (held 29m23s) · pnpm --filter @objectstack/dogfood test · Test Files 419 passed / 3 skipped (422) · Tests 3492 passed / 36 skipped (3528) · no failures", "line_budget": "296 changed lines (+294 / -2) across 4 files, under the 3000 human-merge threshold", "files_changed": [ "packages/rest/src/approvals-routes.ts", "packages/rest/src/rest-server-approvals-status-value.test.ts", "packages/rest/src/rest-server-approvals-unknown-filter.test.ts", ".changeset/29880-approvals-list-unknown-status.md" ], "deviations": [ "Allowed set read from APPROVAL_STATUSES (the contract constant the object's select options are built from), not by a runtime metadata read of sys_approval_request: same single source, no per-poll metadata read and no failure arm. Seat agreed 12:40Z.", "Comma list now reaches the service as an array (a widening half beside the narrowing); without it the SDK's array call would turn from silent [] into 400. Seat agreed 12:40Z.", "The full workspace build before the gates ran outside os-verify-lock (the dogfood battery ran under it).", "main moved 18 commits after the merge (rest overlap: one unrelated test file, analytics-platform-scope-predicate-guard.test.ts). Not re-merged; head held for the contract review.", "Reviewer request and assignee not written (order: no MCP write; relay pr_create sent no assignees)." ], "mcp_calls": "0 — no GitHub MCP call of any kind", "api_writes": "3 — git push (branch), relay pr_create (#30491, 7415 bytes sent = stored), this os-dev-report via post-stamped", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "class: none filed · carrier: PR Acceptance notes · ui/apps/console/src/pages/system/ApprovalsInboxPage.tsx status picker offers pending/approved/rejected/recalled/returned but not cancelled, which the route and the object both have (ui lane, not touched)." ] }Report taken 2026-11-05T13:55Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
Landed · PR #30491 · card closed
75bbebb27;Fixes #29880closed this card. Seat record 6660126726 on head976da77f(BREAKING, not governed, ≤ 3000 lines; contract review PASS 6659996154; landed per pm-dispatch on the maintainer's 13:47Z word).mainsourcepackages/rest/src/approvals-routes.ts:365const statusRead = readApprovalStatusFilter(q.status);status, and the approvals docs page missingcancelled) is unblocked now that this PR is onmain.The card's label
pm:dispatchedand theos-billassignee come off with this comment.Posted 2026-11-05T15:26Z.