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
metadata: an action saved through the metadata door keeps requiresFeature / requiresMembershipReach raw — never lowered into visible — so the button shows to everyone (save-door sibling of #25744) #26723
Filing gate: ① product defect, reproduced, landing site named.
Batch-of: studio-qa-2026-10-27 — 39 cards from one Studio QA run (coordinator session session_01J8VVAkZns5rNnK2rtszzbQ; tested at 3fcafcd3; origin/main was 43731031 at filing, so the implementer re-checks the landing site on current main). The run record is the qa-run issue filed with this batch.
Symptom
The two gates are a parse transform into visible; the metadata save path stores the input without that transform, so a gated action is offered to users who lack the feature or the membership grade.
Reproduction
On gapdata_order: action sso_only with requiresFeature: "ssoEnforced" (false in /auth/config) and action owner_only with requiresMembershipReach: "delete_organization"; save via Studio or PUT /api/v1/meta/object/…?mode=draft, publish.
As a plain member: both buttons are shown. Repeated on open_docs / open_form with the same result.
Expected
Every door that stores an action applies the same lowering (or refuses), so the declared gate is enforced in the console.
Suspected landing site (the implementer verifies)
packages/spec/src/ui/action.zod.ts (~lines 2263-2269, the lowering transform) vs the metadata save path that stores unparsed input — same class as closed #25744 (raw manifest / strict:false path).
Filing facts
reach: Studio + console, measured twice by the data gap-fill card.
Business reason: A declared capability that the runtime does not honour is exactly what the "declared ⇒ enforced" rule forbids; members see actions meant for owners.
Reader who acts: the triage seat (first touch: domain, area, level), then that lane's os-dev implementer. QA suggestion only: domain:spec, area:records, p2.
p2, measure first: an action saved through the metadata door keeps requiresFeature / requiresMembershipReach raw, and they are never lowered into visible. Apply the same parse transform on that path. The dev also measures whether the action endpoint enforces the gate. If it does not, the button is the only gate, and the card goes to p1 · security. From the studio-qa-2026-10-27 batch. Re-check the landing site on current main.
Claim: PM loop round 4 · card thread · 18.2.0 wave-2 split with server seat 1
Session: session_0186vfjHHMsRXC9EUT9hTFMo (the seat); the card thread is session_01FEkQ5kBygwShyUWpFansVP
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-ox9ix0 (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: triage 6446997136 (p2, measure first: apply the same parse transform on the metadata save path, and measure whether the action endpoint enforces the gate; if not, p1 · security); seat ruling: one lowering (ActionSchema's parse, the #25981 path), ⛔ no second copy; the endpoint is measured and reported, ⛔ not widened into without the seat
File surface: the /meta object save path in packages/metadata-protocol/src/** (or packages/objectql/** where #25981 stores the parse), a standalone action save if the door stores one, their tests, and a changeset per package. ⛔ No packages/spec/** unless the transform itself is wrong (then stop and report), ⛔ no endpoint change without the seat, ⛔ no migration of stored rows
Container & model: M, mode:thread, default model and effort (set at thread open)
Clause-②: no (likely: a stored action gains its lowered visible; the dev measures and declares the arm)
Metadata-protocol: none
Responsibility: metadata-protocol save door vs ActionSchema lowering (save-door sibling of #25744)
Thread-read: 6446997136
Premise: holds (read 2026-10-27T18:48Z at origin/main4c29f38447: card open, unassigned, pm:queue with its triage line; the split with server seat 1 gives this card to seat 2); the dev re-measures it first, and a refutation is a premise_still_valid: false hand-back
Resume-from: none — fresh from origin/main
Serial constraints cleared: yes — the dev re-measures open PRs on the file surface before the first edit and before pr_create
State: pm:queue → pm:dispatched, assigned to os-project-manager, in this act.
Measured by the implementer (code at origin/main4c29f38447; a live probe follows in the report): POST /api/v1/actions/:object/:action does not enforce requiresFeature or requiresMembershipReach, lowered or raw.
It checks only requiredPermissions (actionPermissionError, packages/runtime/src/action-execution.ts:832), the activation switch and the type.
visible is unread there by contract (action-execution.ts:2484, domains/actions.ts:166: "visible and disabled are rendering predicates that no dispatch door reads").
So for every action, packaged ones included, the button is the only gate unless the author also declares requiredPermissions.
Relabelled per triage 6446997136:priority:p2 → priority:p1, plus security.
The ruling. The maintainer's standing line for security cards is 「所有的安全卡不要创建issue,直接派发处理修复」, so the endpoint half is dispatched on this card, not filed as a new one:
Endpoint. The action dispatch door refuses a caller who fails the action's declaredrequiresFeature or requiresMembershipReach, with the existing refusal code the door uses for requiredPermissions (⛔ no new code unless none fits; then stop and report).
visible stays a rendering predicate, unread by dispatch. Contract point 6 is unchanged; this card enforces the two declared gate keys, not arbitrary visible expressions.
Read the gate from the one evaluator the lowering targets: the feature flag from /auth/config's source and the membership grade from MEMBERSHIP_REACH. ⛔ No second spelling of either rule.
If the parsed action no longer carries the raw keys after lowering, so that dispatch cannot see what was declared, stop and report the shape with options. ⛔ No packages/spec/** change without the seat.
This is Clause-②: yes (narrowing) for the endpoint, with the Skills-docs: line and the full dogfood battery.
One PR or two: the implementer's call by size. If two, both carry Fixes/Part of #26723 and the save-door PR may land first.
Four axes:
实际业务需求: QA measured both gates shown to a plain member, and nothing behind the button stops the call.
项目长远合理性: a declared gate that the server does not enforce is a declared-but-unenforced capability; the fix is to enforce it where declared.
防 AI 写错: an AI author who declares requiresMembershipReach reasonably believes it guards the action. 声明即强制.
创业阶段不扩散需求: no new key, no new gate, only enforcement of two keys that already exist.
The implementer hit the stop condition that ruling set and measured the options. The analysis is in the project file pm-orders/server/26723-endpoint-options.md.
Shape:lowerRequiresFeature and lowerRequiresMembershipReach both return Omit<T, key>, so a parsed action carries only the composed visible. Dispatch cannot see the declared gate without a packages/spec/** change.
The spec already rules both keys as UI courtesy:
action.zod.ts:1553: "Client-side hiding is UX, not authorization … must also be refused server-side (requiredPermissions, or the action's own body)";
action.zod.ts:1572: "the endpoint's own door stays the authority";
membership-reach.ts:61 says the same.
Every in-repo producer of either key, about 45 actions in platform-objects identity, is type: 'api'. Each calls a better-auth endpoint that checks the grade or flag itself. POST /api/v1/actions/:object/:action refuses api and url actions with a 400 (headlessActionTypeError). There are zero carriers in examples/** and HotCRM.
So no gated action that exists today is reachable through the dispatch door, and each one's own door is the authority. The defect the QA run measured, a button shown to the wrong member, is the save door's missing lowering, and the save-door PR closes it.
Ruling: A. No dispatch enforcement, and no packages/spec/** change. B would add a second authority path for two sugar keys that nothing pulls, and C is already ruled out (contract point 6).
实际业务需求: B guards zero producers.
项目长远合理性: the spec already places authority at the endpoint's door and at requiredPermissions.
防 AI 写错: the spec docs already say the keys are not a guard.
创业阶段不扩散需求: no enforcement without a pull, and 新增门禁默认否.
Labels: reverted to priority:p2, and security removed. The p1 · security arm of triage 6446997136 assumed a gate the button alone carried. The measurement shows each gated action's real door enforces the gate itself.
os-dev-report
{
"issue": 26723,
"status": "done",
"branch": "claude/project-thread-ox9ix0",
"pr": "#26880",
"session": "session_01FEkQ5kBygwShyUWpFansVP",
"premise_still_valid": true,
"summary": "ENDPOINT: POST /api/v1/actions/:object/:action does NOT enforce requiresFeature / requiresMembershipReach or the lowered visible. It checks requiredPermissions, the activation switch and the type only, and visible is unread by contract (action-execution.ts:2484, pinned by action-declarative-update.test.ts point 6). The parsed action drops both raw keys, so dispatch cannot see them without a spec change. Seat ruled option A (comment 6450896632): no dispatch enforcement, no spec change, card back to p2. SAVE DOOR (this PR): saveMetaItem stored an object or action body as written after judging it by its schema, so the gate keys stayed raw with no visible. It now stores the parse it already ran (new withParsedStoredActions in protocol-stored-action-parse.ts, one call in protocol.ts): an object's own actions array becomes the parsed array, and a standalone action becomes the parsed action. This covers draft and active, and is the same parse the code-package door stores since #25981. Stored rows are not migrated. The showcase, the examples and HotCRM carry 0 such actions in source; a re-save fixes an old Studio row.",
"tests": "New protocol.stored-action-gate-lowering.test.ts (7 tests: object draft and publish lowered and deep-equal to ObjectSchema.create, plain-action control, idempotent re-save, no-actions object, standalone action draft and publish). Red: protocol.ts at base blob 4ccf8acedb gives 5 failed / 2 passed (the 2 are the idempotency and no-actions controls); restored blob 92f1deab22 == HEAD, git diff HEAD empty. Ablation via ablation-replace.mjs --delete of the one call line (blob 92f1deab22 -> 2ecf252721, anchor 1 -> 0) gives 5 failed / 2 passed; restored blob == HEAD, diff empty. The test imports source, so there is no dist leg. metadata-protocol full suite at a55b8f4: Test Files 271 passed | 4 skipped, Tests 21669 passed | 26 skipped, exit 0; typecheck 0; build 0. Consumer files after building the runtime^... and rest^... closures: runtime 8 files, 544 tests passed; rest rest.test.ts, 215 passed. Endpoint pin action-declarative-update.test.ts -t point 6: 2 passed. eslint --no-inline-config on the 3 touched files: exit 0.",
"gates": "dispatch-gates --commands at 91803f6 derived 69 families; all run; --ran reconciles 69 accounted, 68 run, 1 NOT-MEASURED: check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET (needs every dist). check:engine-double-contract was red on the first run (new double unrecorded), and the ledger was updated with --write in commit 91803f6, now exit 0. check-plugin-teardown-shape --self-test exited 3 on the shallow clone, then passed 48 cases after fetching its control commit. CI check-runs are in progress at report time; the seat reads them.",
"dogfood_battery": "n/a: no ! in the PR title, and Clause-② is no with no (narrowing) arm",
"mcp_calls": "11 MCP GitHub calls, all reads: issue_read x5 (get, get_comments x4), get_commit x1, search_pull_requests x1, pull_request_read get_files x1, plus 3 ToolSearch loads (not GitHub calls). No write tool.",
"api_writes": "2 so far: relay pr_create (#26880, assignee os-project-manager) and this os-dev-report comment through post-stamped. git push is not counted.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none, noted, not filed. The spec says requiresFeature / requiresMembershipReach are UI courtesy, and an AI author may still read them as a server guard. The seat ruling A already covers this, with no new gate. Dedupe words: requiresMembershipReach authorization, action gate server enforcement, visible not authorization."
]
}
PR #26880 merged at 2026-10-27T20:46:53Z by the merge queue as 57e00f5b99 on main (Fixes #26723; ACCEPT 6452435998; Clause-②: no). GitHub closed the card as completed.
Endpoint (ruling 6450896632, option A):POST /api/v1/actions/:object/:action does not enforce either key. The spec documents both as UI courtesy. Every in-repo carrier is a type: 'api' action whose own endpoint is the authority, and the dispatch door answers 400 for api and url actions.
Not migrated: a row saved through Studio before this fix keeps the raw keys until it is saved again.
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: first grade —
priority:p2·domain:server·area:access·pm:queue(findingremoved) · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T16:06Z⛔ Not a claim, ⛔ not a dispatch.
requiresFeature/requiresMembershipReachraw, and they are never lowered intovisible. Apply the same parse transform on that path. The dev also measures whether the action endpoint enforces the gate. If it does not, the button is the only gate, and the card goes to p1 ·security. From the studio-qa-2026-10-27 batch. Re-check the landing site on currentmain.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 4 · card thread · 18.2.0 wave-2 split with server seat 1
Session:
session_0186vfjHHMsRXC9EUT9hTFMo(the seat); the card thread issession_01FEkQ5kBygwShyUWpFansVPAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-ox9ix0(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: triage
6446997136(p2, measure first: apply the same parse transform on the metadata save path, and measure whether the action endpoint enforces the gate; if not, p1 ·security); seat ruling: one lowering (ActionSchema's parse, the #25981 path), ⛔ no second copy; the endpoint is measured and reported, ⛔ not widened into without the seatFile surface: the
/metaobject save path inpackages/metadata-protocol/src/**(orpackages/objectql/**where #25981 stores the parse), a standaloneactionsave if the door stores one, their tests, and a changeset per package. ⛔ Nopackages/spec/**unless the transform itself is wrong (then stop and report), ⛔ no endpoint change without the seat, ⛔ no migration of stored rowsContainer & model:
M,mode:thread, default model and effort (set at thread open)Clause-②: no (likely: a stored action gains its lowered
visible; the dev measures and declares the arm)Metadata-protocol: none
Responsibility:
metadata-protocolsave door vsActionSchemalowering (save-door sibling of #25744)Thread-read: 6446997136
Premise: holds (read 2026-10-27T18:48Z at
origin/main4c29f38447: card open, unassigned,pm:queuewith its triage line; the split with server seat 1 gives this card to seat 2); the dev re-measures it first, and a refutation is apremise_still_valid: falsehand-backResume-from: none — fresh from
origin/mainSerial constraints cleared: yes — the dev re-measures open PRs on the file surface before the first edit and before
pr_createState:
pm:queue→pm:dispatched, assigned toos-project-manager, in this act.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Seat ruling: #26723 goes to p1 ·
security, and the endpoint half rides this card2026-10-27T18:52Z ·
domain:serverseat 2 ·os-project-manager· sessionsession_0186vfjHHMsRXC9EUT9hTFMo· claim6450751670Measured by the implementer (code at
origin/main4c29f38447; a live probe follows in the report):POST /api/v1/actions/:object/:actiondoes not enforcerequiresFeatureorrequiresMembershipReach, lowered or raw.requiredPermissions(actionPermissionError,packages/runtime/src/action-execution.ts:832), the activation switch and the type.visibleis unread there by contract (action-execution.ts:2484,domains/actions.ts:166: "visible and disabled are rendering predicates that no dispatch door reads").So for every action, packaged ones included, the button is the only gate unless the author also declares
requiredPermissions.Relabelled per triage
6446997136:priority:p2→priority:p1, plussecurity.The ruling. The maintainer's standing line for security cards is 「所有的安全卡不要创建issue,直接派发处理修复」, so the endpoint half is dispatched on this card, not filed as a new one:
ActionSchemaparse, as fix(objectql)!: store an object's own actions as their ActionSchema parse, so a raw manifest's requiresFeature is lowered #25981 does. ⛔ No second copy of the lowering.requiresFeatureorrequiresMembershipReach, with the existing refusal code the door uses forrequiredPermissions(⛔ no new code unless none fits; then stop and report).visiblestays a rendering predicate, unread by dispatch. Contract point 6 is unchanged; this card enforces the two declared gate keys, not arbitraryvisibleexpressions./auth/config's source and the membership grade fromMEMBERSHIP_REACH. ⛔ No second spelling of either rule.packages/spec/**change without the seat.Clause-②: yes (narrowing)for the endpoint, with theSkills-docs:line and the full dogfood battery.Fixes/Part of #26723and the save-door PR may land first.Four axes:
requiresMembershipReachreasonably believes it guards the action. 声明即强制.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Seat ruling: #26723's endpoint half → A, no dispatch enforcement; the card returns to p2
2026-10-27T18:55Z ·
domain:serverseat 2 ·os-project-manager· sessionsession_0186vfjHHMsRXC9EUT9hTFMo· supersedes item 2 of6450826243The implementer hit the stop condition that ruling set and measured the options. The analysis is in the project file
pm-orders/server/26723-endpoint-options.md.lowerRequiresFeatureandlowerRequiresMembershipReachboth returnOmit<T, key>, so a parsed action carries only the composedvisible. Dispatch cannot see the declared gate without apackages/spec/**change.action.zod.ts:1553: "Client-side hiding is UX, not authorization … must also be refused server-side (requiredPermissions, or the action's own body)";action.zod.ts:1572: "the endpoint's own door stays the authority";membership-reach.ts:61says the same.platform-objectsidentity, istype: 'api'. Each calls a better-auth endpoint that checks the grade or flag itself.POST /api/v1/actions/:object/:actionrefusesapiandurlactions with a 400 (headlessActionTypeError). There are zero carriers inexamples/**and HotCRM.So no gated action that exists today is reachable through the dispatch door, and each one's own door is the authority. The defect the QA run measured, a button shown to the wrong member, is the save door's missing lowering, and the save-door PR closes it.
Ruling: A. No dispatch enforcement, and no
packages/spec/**change. B would add a second authority path for two sugar keys that nothing pulls, and C is already ruled out (contract point 6).requiredPermissions.Labels: reverted to
priority:p2, andsecurityremoved. The p1 · security arm of triage6446997136assumed a gate the button alone carried. The measurement shows each gated action's real door enforces the gate itself.The save-door PR says
Fixes #26723.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{
"issue": 26723,
"status": "done",
"branch": "claude/project-thread-ox9ix0",
"pr": "#26880",
"session": "session_01FEkQ5kBygwShyUWpFansVP",
"premise_still_valid": true,
"summary": "ENDPOINT: POST /api/v1/actions/:object/:action does NOT enforce requiresFeature / requiresMembershipReach or the lowered visible. It checks requiredPermissions, the activation switch and the type only, and visible is unread by contract (action-execution.ts:2484, pinned by action-declarative-update.test.ts point 6). The parsed action drops both raw keys, so dispatch cannot see them without a spec change. Seat ruled option A (comment 6450896632): no dispatch enforcement, no spec change, card back to p2. SAVE DOOR (this PR): saveMetaItem stored an object or action body as written after judging it by its schema, so the gate keys stayed raw with no visible. It now stores the parse it already ran (new withParsedStoredActions in protocol-stored-action-parse.ts, one call in protocol.ts): an object's own actions array becomes the parsed array, and a standalone action becomes the parsed action. This covers draft and active, and is the same parse the code-package door stores since #25981. Stored rows are not migrated. The showcase, the examples and HotCRM carry 0 such actions in source; a re-save fixes an old Studio row.",
"tests": "New protocol.stored-action-gate-lowering.test.ts (7 tests: object draft and publish lowered and deep-equal to ObjectSchema.create, plain-action control, idempotent re-save, no-actions object, standalone action draft and publish). Red: protocol.ts at base blob 4ccf8acedb gives 5 failed / 2 passed (the 2 are the idempotency and no-actions controls); restored blob 92f1deab22 == HEAD, git diff HEAD empty. Ablation via ablation-replace.mjs --delete of the one call line (blob 92f1deab22 -> 2ecf252721, anchor 1 -> 0) gives 5 failed / 2 passed; restored blob == HEAD, diff empty. The test imports source, so there is no dist leg. metadata-protocol full suite at a55b8f4: Test Files 271 passed | 4 skipped, Tests 21669 passed | 26 skipped, exit 0; typecheck 0; build 0. Consumer files after building the runtime^... and rest^... closures: runtime 8 files, 544 tests passed; rest rest.test.ts, 215 passed. Endpoint pin action-declarative-update.test.ts -t point 6: 2 passed. eslint --no-inline-config on the 3 touched files: exit 0.",
"gates": "dispatch-gates --commands at 91803f6 derived 69 families; all run; --ran reconciles 69 accounted, 68 run, 1 NOT-MEASURED: check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET (needs every dist). check:engine-double-contract was red on the first run (new double unrecorded), and the ledger was updated with --write in commit 91803f6, now exit 0. check-plugin-teardown-shape --self-test exited 3 on the shallow clone, then passed 48 cases after fetching its control commit. CI check-runs are in progress at report time; the seat reads them.",
"dogfood_battery": "n/a: no ! in the PR title, and Clause-② is no with no (narrowing) arm",
"mcp_calls": "11 MCP GitHub calls, all reads: issue_read x5 (get, get_comments x4), get_commit x1, search_pull_requests x1, pull_request_read get_files x1, plus 3 ToolSearch loads (not GitHub calls). No write tool.",
"api_writes": "2 so far: relay pr_create (#26880, assignee os-project-manager) and this os-dev-report comment through post-stamped. git push is not counted.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none, noted, not filed. The spec says requiresFeature / requiresMembershipReach are UI courtesy, and an AI author may still read them as a server guard. The seat ruling A already covers this, with no new gate. Dedupe words: requiresMembershipReach authorization, action gate server enforcement, visible not authorization."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Landed
2026-10-27T20:47Z ·
domain:serverseat 2 ·os-project-manager· sessionsession_0186vfjHHMsRXC9EUT9hTFMoPR #26880 merged at 2026-10-27T20:46:53Z by the merge queue as
57e00f5b99onmain(Fixes #26723; ACCEPT6452435998;Clause-②: no). GitHub closed the card as completed.git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-26880-*'git merge-base --is-ancestor 57e00f5b99 origin/mainorigin/mainby contentpackages/metadata-protocol/src/protocol.tswithParsedStoredActionsActionSchemaparse. This applies to an object's ownactionsand to a standaloneaction. A declaredrequiresFeature/requiresMembershipReachis lowered intovisible, so the console hides a gated button the same way it hides one from a code package (fix(objectql)!: store an object's own actions as their ActionSchema parse, so a raw manifest's requiresFeature is lowered #25981).6450896632, option A):POST /api/v1/actions/:object/:actiondoes not enforce either key. The spec documents both as UI courtesy. Every in-repo carrier is atype: 'api'action whose own endpoint is the authority, and the dispatch door answers 400 forapiandurlactions.Generated by Claude Code