Skip to content

feat(spec, platform-objects): declare features.cloudManaged with a hide-when-on gate, and hide the six doors that add people while it is on - #25309

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-4kg5av
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-4kg5av

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

Fixes #25292
Fixes #25295
Part of #25083

Clause-②: yes (widening)
Metadata-protocol: added

The console flag features.cloudManaged for the managed (cloud-hosted, SSO-only) mode, built as one trunk under ruling 6352436010 on #25292 (letter A, the cross-domain exception path): the spec entry, the six platform-objects predicates and plugin-auth's served line are held equal by three pins, so they land in one commit. The governing ruling for the flag itself is 6341690661 on #25083, Q3 A (and Q4 A for set_user_password).

Spec half (#25292)

  • PublicAuthFeatureSemantics gains 'hide-when-on': the flag being ON hides the input. featureGatePredicate lowers it to features.X != true. The function now reads a total map over the semantics, so a fourth semantics fails to compile instead of taking another arm's operator.
  • PUBLIC_AUTH_FEATURES.cloudManaged (surface: 'crud', semantics: 'hide-when-on') books six gated inputs: invite_user on sys_user, sys_member, sys_invitation, sys_organization; sys_user.actions.create_user; sys_user.actions.set_user_password. Through PUBLIC_AUTH_FEATURE_NAMES, requiresFeature: 'cloudManaged' is now accepted and lowers to features.cloudManaged != true (this is the Metadata-protocol: added).
  • AuthFeaturesConfigSchema.cloudManaged: z.boolean().optional(), the form of the newest siblings (ssoEnforced, phoneNumber, phoneNumberOtp).
  • Regenerated: content/docs/references/{api/auth-endpoints,ui/action}.mdx, one line of packages/spec/authorable-surface/api.json (check:generated --fix proved only check:docs stale; api-surface unchanged). content/docs/protocol/objectui/actions.mdx names the third lowering.

Engine half (#25295)

  • Each of the six actions keeps its requiresFeature (organization x4, admin x2) and gains features.cloudManaged != true written in visible, since requiresFeature names one flag. Lowered, e.g. (current_user.isPlatformAdmin == true && features.cloudManaged != true) && features.admin == true.
  • set_user_password is hidden in the console only; no server-side refusal here (the server keeps answering it on an env_native row).
  • feature-gate-guard.test.ts: the forward rows now ask whether the flag's gate is one of the predicate's && conjuncts. Parentheses that only group conjuncts are transparent; a gate under ||, ! or ?: is not a gate; no substring match. Every row accepted before is accepted unchanged. Pinned in the same file: accepted gate, (a) && gate, (a && gate) && b and the invite shape; refused a || gate, a || b && gate, !(gate), a ? gate : true, a longer token, and the gate inside a string literal.
  • identity/cloud-managed-visibility.test.ts evaluates the six lowered predicates through @objectstack/formula's celEngine: hidden with cloudManaged: true, offered with false, and still hidden when its other flag is off.
  • Byte pins updated for the new predicates: platform-objects.test.ts (lowering matrix), invite-entry-toolbar.test.ts (the mirrors agree); platform-admin-affordance-standing.test.ts serves cloudManaged: false beside the other flags.

Services line (part of #25083)

Verification (head 72cea5b1)

  • main merged in (no rebase) after core: isCloudManaged() — one accessor for OS_CLOUD_MANAGED=1 beside isHostKernelComposition, with the production fuse (no bound control plane ⇒ refuse to start) #25294 landed as db1c7ccad.
  • @objectstack/spec: check:generated reports all 14 generated artifacts up to date after rebuilding spec on the merged tree. Spec suite on d30caab4: 783 files, 22794 passed, 1 todo; typecheck 0.
  • @objectstack/plugin-auth: full vitest run on 91d3cd6b was 186/187 files, the one red being the exact-match payload test missing cloudManaged (fixed in 72cea5b1); on 72cea5b1 auth-manager.test.ts + public-feature-registry.test.ts 302/302 passed, so the registry drift guard is green; typecheck 0.
  • Reverse verification of the served line: replacing cloudManaged: isCloudManaged() with cloudManaged: false on disk reds serves features.cloudManaged from OS_CLOUD_MANAGED (1 failed); file restored from a copy, git diff empty.
  • @objectstack/platform-objects: vitest run 88 files, 1318 passed; typecheck 0.
  • Reverse verification of the guard, both legs through scripts/ablation-replace.mjs, restored to the HEAD blob each time: deleting the authored gate from sys_member.invite_user reds sys_member.actions.invite_user is gated on cloudManaged (1 failed / 100 passed); replacing it with true || features.cloudManaged != true reds the same row (1 failed / 101 passed).
  • Before this branch's engine half, the spec half alone on the base reds 6 forward rows in the guard and 2 plugin-auth registry cases (measured, dependency closure built).
  • Derived gates on 72cea5b1: dispatch-gates --commands lists 117; 116 exit 0; 1 exit 3 (PREREQUISITE NOT MET, left to CI): check-plugin-teardown-shape --self-test (shallow clone cannot read its pinned fixture commit). --ran reconciles 117 derived, 116 run, 1 NOT-MEASURED, 0 UNRUN.
  • CI on 72cea5b1: 36 check runs success, 6 skipped, none red (Test Core 1–6, Build Core, Build Docs, every Lint and Type Check lane, and The card this PR closes must claim this branch).

Acceptance notes

  • The spec comments say an absent default-on flag "fails open". In @objectstack/formula's celEngine an absent key faults (No such key) for every flag, default-on included, so the answer for a payload that omits a flag is the consumer's fault handling. Not changed here; plugin-auth's drift guard keeps the served key set stable, so a current server never omits one.
  • A Clause-② (narrowing) was looked for and not found: the six actions only hide where the server serves cloudManaged: true, which happens only on a runtime with OS_CLOUD_MANAGED=1 (the cloud-hosted one, whose people arrive through the identity provider); every other deployment serves false and sees no change.

Changesets: @objectstack/spec minor (Clause-②: yes (widening), Metadata-protocol: added); @objectstack/platform-objects patch (Clause-②: no, Metadata-protocol: none); @objectstack/plugin-auth patch (Clause-②: yes (widening), Metadata-protocol: none).

Review: CONTRACT_REVIEW_TIER (schema face) by the domain:spec seat; the domain:engine seat posts one PASS/FAIL line on the platform-objects hunks. #25294 is on main; draft until the maintainer has confirmed ruling 6352436010.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P9sxzbFDWjQD3FdsL3mN4u


Generated by Claude Code

…or features.cloudManaged

Claude-Session: https://claude.ai/code/session_01P9sxzbFDWjQD3FdsL3mN4u
Co-authored-by: Claude <noreply@anthropic.com>
…ures.cloudManaged is on; the forward guard finds a gate among && conjuncts

Claude-Session: https://claude.ai/code/session_01P9sxzbFDWjQD3FdsL3mN4u
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/plugin-auth, @objectstack/spec, touching 7 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/permissions/sso.mdx (via getPublicConfig (symbol, a method of class AuthManager))
  • content/docs/protocol/objectui/actions.mdx (via PUBLIC_AUTH_FEATURES (symbol, a top-level const object), cloudManaged (symbol, a field of const object PUBLIC_AUTH_FEATURES))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via PUBLIC_AUTH_FEATURES (symbol, a top-level const object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 148 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9c89cf5b79d343fbeec9d7bfd99fcc8476fa1a03 — the merge of head 72cea5b1ef8d645b9b5e004fd95037e39e021f09 into base 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9c89cf5b79d343fbeec9d7bfd99fcc8476fa1a03 && git checkout 9c89cf5b79d343fbeec9d7bfd99fcc8476fa1a03
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a 72cea5b1ef8d645b9b5e004fd95037e39e021f09 && git checkout -B drift-repro 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a && git merge --no-ff 72cea5b1ef8d645b9b5e004fd95037e39e021f09

node scripts/docs-audit/affected-docs.mjs --json 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9dc1f42d50b2ee0c0afe8b6b7d7922b7c912f83a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

engine seat 2 · platform-objects hunk · PASS · session_01FaVyM6YvQLg9CsjPVCYc5K · 2026-10-22T23:01Z · head d30caab47b · ruling 6352436010 item 8. Checked: each of the six visible (four invite_user mirrors, create_user, set_user_password) hand-writes features.cloudManaged != true, and every requiresFeature still names one flag (organization / admin). The lowered rows pin ((MANAGED) && INVITE) && ORG and (STANDING && MANAGED) && features.admin == true. The forward guard replaces === gate || endsWith('&& gate') with a depth-0 && conjunct match that stops at || / ?: / !( ) and at string literals; its pins accept gate, (a) && gate, (a && gate) && b and reject a || gate. I also ran the matcher on its own against 12 cases, including (x || gate) && y → false and (a || b) && gate → true, and all 12 agree. set_user_password is hidden in the console only: this hunk has no server-side refusal, and the registry note says the same. Scope: this line covers platform-objects only; the plugin-auth served row and the spec half are not part of it.

…load

The exact-match payload test now lists `cloudManaged: false` (OS_CLOUD_MANAGED
unset), and a new case sets OS_CLOUD_MANAGED=1 and reads `true` back through
`isCloudManaged()`, resetting the memoized verdict on both sides.

Claude-Session: https://claude.ai/code/session_01P9sxzbFDWjQD3FdsL3mN4u
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 72cea5b1ef8d645b9b5e004fd95037e39e021f09
Local-runs: none
Scope: packages/spec/src/api/auth-endpoints.zod.ts, packages/spec/src/kernel/public-auth-features.ts + packages/spec/src/ui/action.zod.ts, packages/spec/src/kernel/public-auth-features.test.ts, packages/spec/src/ui/action.test.ts, packages/spec/authorable-surface/api.json, packages/platform-objects/src/feature-gate-guard.test.ts, packages/platform-objects/src/identity/sys-user.object.ts, packages/platform-objects/src/identity/sys-member.object.ts, packages/platform-objects/src/identity/sys-invitation.object.ts, packages/platform-objects/src/identity/sys-organization.object.ts, packages/platform-objects/src/identity/cloud-managed-visibility.test.ts, packages/platform-objects/src/platform-objects.test.ts, packages/plugins/plugin-auth/src/auth-manager.ts, packages/plugins/plugin-auth/src/auth-manager.test.ts, packages/plugins/plugin-auth/src/public-feature-registry.test.ts, packages/core/src/stack-auth.ts (on main, #25294), packages/formula/src/cel-engine.ts (on main), .changeset/25292-features-cloud-managed.md, .changeset/25292-cloud-managed-hidden-doors.md, .changeset/25292-plugin-auth-serves-cloud-managed.md, content/docs/protocol/objectui/actions.mdx

Inputs: card #25292 (body; comments 6351958701 claim, 6352436010 ruling A, 6352529197 claim amendment, 6353227863 supplement, 6354907704 os-dev-report), governing ruling 6341690661 on #25083 (Q3 A, Q4 A, Q2 D), card #25295 (body; 6352506874 pointer, 6353249248 claim), PR #25309 body, its 22-file list, the net diff against main at the head, the engine seat's PASS line 6353213686 (head d30caab47b), and the head's check-runs: 42 success, 12 skipped, 0 failure (Test Core 1–6, Build Core, Build Docs, every Lint and Type Check lane, Check Changeset, Spec property liveness, Governed Surface Queue Guard, The card this PR closes must claim this branch, No other open PR may claim the same issue, Part-of PR must not also close its card — all success).

Precondition of ruling A (#25294 on main before the served line): holds. db1c7ccad (feat(core): isCloudManaged(), #25306) is an ancestor of origin/main and of the PR's merge-base 21a25dd9; the branch merged main at 840a8ad and added the served line after it (91d3cd6, 72cea5b1) — no stacking off #25294's branch. packages/core/src/stack-auth.ts:172 exports isCloudManaged() and :183 resetCloudManagedForTesting(), both on main.

① Derived judgments

Spec half (the hits), each read against ruling 6341690661 Q3 A and ruling 6352436010 items 1–6:

  1. public-auth-features.ts:64 — PublicAuthFeatureSemantics gains 'hide-when-on'. A public type widening of an already-exported union; api-surface/kernel.json lists the same export and is unchanged. Right.
  2. public-auth-features.ts:336-350 — FEATURE_GATE_OPERATORS is a total satisfies Record map over the three semantics; featureGatePredicate('cloudManaged') returns features.cloudManaged != true, and opt-in / default-on keep == true / != false byte-identically (pinned at public-auth-features.test.ts:76-88). Exactly the predicate the ruling binds. Right.
  3. public-auth-features.ts:237-258 — PUBLIC_AUTH_FEATURES.cloudManaged (surface: 'crud', semantics: 'hide-when-on'), six gatedInputs: invite_user on sys_user / sys_member / sys_invitation / sys_organization, sys_user.actions.create_user, sys_user.actions.set_user_password. The list the ruling re-verified and platform-objects: hide invite_user ×4, create_user and set_user_password when features.cloudManaged is on (visible predicates, hide-when-on) #25295's Wanted names. Through PUBLIC_AUTH_FEATURE_NAMES (:275) the requiresFeature enum on ActionSchema (action.zod.ts:1498) and ActionParamSchema (:458) now accepts 'cloudManaged' and lowers it to features.cloudManaged != true (pinned action.test.ts:185-194). This is the one accept-set widening and the Metadata-protocol: added. Right. PUBLIC_AUTH_FEATURES_NOT_ADVERTISED untouched, no exempt entry, no gatedInputs: [] interim shape — ruling item 9 (C) not taken. Right.
  4. auth-endpoints.zod.ts:242-244 — AuthFeaturesConfigSchema.cloudManaged: z.boolean().optional(), the form of ssoEnforced / phoneNumber / phoneNumberOtp. Optional, so every payload accepted before still parses; .describe() says absent means not managed, which is the hide-when-on reading. authorable-surface/api.json:209 gains api/AuthFeaturesConfig:cloudManaged in sorted position; the two generated reference pages carry the row and the 12-member enum; check:generated is inside the green Lint lane. Right.
  5. The spec test count moves 11 → 12 (public-auth-features.test.ts:19-20) with cloudManaged in the sorted name list. Right.

Files the spec hunks make a claim about (a rule and its gate read as one):

  1. feature-gate-guard.test.ts:61-128 (matcher) and :157-165 (forward row), pins at :168-188 — the forward row now asks carriesGateConjunct(visibleSource, gate): depth-0 split on && outside parentheses, brackets and string literals; wrapping parentheses stripped; a top-level || or ? makes the group opaque; !(gate) is not wrapped so stays opaque; membership is conjunct === gate, no substring. The two shapes accepted before (gate; X && gate) still pass; a || b && gate, which the old endsWith('&& gate') would have accepted, is now refused — correct under CEL precedence and exactly ruling item 3. Pinned: gate, (a) && gate, (a && gate) && b, the four-mirror invite shape; refused a || gate, a || b && gate, !(gate), a ? gate : true, a longer token, the gate inside a string. The reverse row is unchanged, so the six new features.cloudManaged references must be booked — they are. A gate taught a shape, not loosened. Right.
  2. The six predicates — sys-user.object.ts:106 / :250 / :325, sys-member.object.ts:109, sys-invitation.object.ts:73, sys-organization.object.ts:177: each keeps its single requiresFeature (organization ×4, admin ×2) and gains the literal features.cloudManaged != true in visible — the four invite mirrors as the whole authored visible, create_user / set_user_password appended with && to the standing term, nothing else in the predicate changed. Lowered bytes pinned at platform-objects.test.ts:706-707,710,728,744 and invite-entry-toolbar.test.ts:177-179; evaluated through @objectstack/formula's celEngine at cloud-managed-visibility.test.ts:65-77 (true hides all six, false offers all six, the other flag off still hides). set_user_password is console-only — no server refusal in this diff (Q4 A). Right. requiresFeature stays single-flag (ruling item 4). Right.
  3. auth-manager.ts:49 import and :8014-8017 cloudManaged: isCloudManaged() in the features literal — exactly one served line plus its import; nothing else of plugin-auth, setup users: a managed (SSO-only) hosted environment keeps only authorization + view in Setup → Users, turns its local sign-up off, and gives a JIT user the declared default permission set (cloud#3132 ruling A) #25083's services half moves. The existing drift guard (public-feature-registry.test.ts:41-45 key-set equality; :84-96 default — a non-default-on flag serves false, and under vitest OS_CLOUD_MANAGED is unset) is the pin ruling item 5 names and is green. The fuse of Q2 D fires inside isCloudManaged() on a production boot with no control plane; the served line inherits that behaviour rather than adding a second judgement. Right.
  4. Public-surface effect on the console: the six lowered predicates change bytes, but they evaluate identically to today wherever the server serves cloudManaged: false (every deployment without OS_CLOUD_MANAGED=1), and cloudManaged: true was not a reachable state before this PR. The server-side accept set is unchanged. No (narrowing) arm anywhere. Right.

② Semver level

  • @objectstack/spec minor (.changeset/25292-features-cloud-managed.md): Clause-②: yes (widening), Metadata-protocol: added — a new accepted enum value on requiresFeature and a new key on a published response schema; added takes at least minor (AGENTS.md Post-Task step 3). Matches the diff and ruling item 6. Right.
  • @objectstack/platform-objects patch (25292-cloud-managed-hidden-doors.md): Clause-②: no, Metadata-protocol: none — no authorable key, value or type changes; the accept set does not move; none takes patch. Matches the diff and the platform-objects: hide invite_user ×4, create_user and set_user_password when features.cloudManaged is on (visible predicates, hide-when-on) #25295 claim. Right.
  • @objectstack/plugin-auth patch (25292-plugin-auth-serves-cloud-managed.md): Clause-②: yes (widening), Metadata-protocol: none — the wire payload gains a boolean key (public surface widens; nothing an author writes changes); a non-protocol widening ships patch. Matches the diff and ruling item 6's expectation. Right.
  • No skip-changeset; one changeset per package the diff moves; Check Changeset success on the head.
  • Clause-②: the PR body's line-initial Clause-②: yes (widening) and Metadata-protocol: added are the whole-PR declarations the ruling fixed (several kinds ⇒ added over none), and each changeset carries its own pair. Right.

③ Boundary flags

  1. Maintainer 追认 of ruling 6352436010 is still pending. This record judges the diff against the ruling as written; ruling item 8 keeps the PR draft until the 追认 reaches the review thread or the card, and the queue waits on it. Not a verdict matter.
  2. Engine seat's read (6353213686) was on head d30caab47b; git diff d30caab4 72cea5b1 -- packages/platform-objects is empty, so the PASS covers the platform-objects hunks of this head byte-for-byte. Answered.
  3. Dev open_question (PR body replacement). Answered by the seat before this read: the body at this reading carries the Verification section at 72cea5b1, the Services line section, no Blocked-by: line and no two-red window; Fixes #25292, Fixes #25295, Part of #25083 and both declaration lines are line-initial. Nothing left to patch.
  4. Dev finding: "absent flag fails open". public-auth-features.ts:59-62 and :343-346 (and the test title at public-auth-features.test.ts:86, and the spec changeset's "visible when it is off or not served at all") say an absent cloudManaged keeps the input visible. cel-engine.ts:1877 grades no_such_key a runtime (data) fault, so features.cloudManaged != true on a payload without the key faults and the outcome is the consumer's fault handling — which is why platform-admin-affordance-standing.test.ts:172 has to serve cloudManaged: false and why cloud-managed-visibility.test.ts:11-14 pins false, not absent. The same sentence has stood on default-on since feat(spec,auth): guard that every feature-gated capability is UI-gated (generalize the create-user phone fix) #2874; the predicate the ruling binds is exact; the drift guard means a current server never omits a registered flag; and the schema's own .describe() ("absent means not managed") is a payload statement that objectui's hand-written readers honour. Not a contract defect of this diff. Escalated to the adopting seat as a follow-up card for the three sentences (or a one-clause trim of the changeset line before landing); platform-objects: hide invite_user ×4, create_user and set_user_password when features.cloudManaged is on (visible predicates, hide-when-on) #25295's Acceptance wording "false or absent" is met for false and is not achievable at the engine for absent on any flag.
  5. Stale jsdoc, in scope. action.zod.ts:452 and :1494 still enumerate only == true (opt-in) and != false (default-on). The claim amendment's surface included spec-side readers that must learn the third arm; these are prose on the enum's own schema, not code, and the published page (content/docs/protocol/objectui/actions.mdx:370) was updated. Flagged for the adopting seat: fold the two comment lines before landing or leave to the follow-up in item 4; not a verdict matter.
  6. One test beyond the allocated surface. auth-manager.test.ts:3186-3187 (the exact-match payload gains cloudManaged: false) was forced — the suite was red without it. :3196-3211 adds a "true when OS_CLOUD_MANAGED=1" case; ruling item 5 placed the "true when managed" pins with the services half (6349623693). Additive, correct (vitest runs under NODE_ENV=test, so the fuse does not fire; the env and memo are restored), and named in the PR body, so the services seat is told; the only cost is a possible textual overlap when that branch merges main. Noted, not a breach of the contract.
  7. Dev finding: docs drift content/docs/permissions/sso.mdx. Read: line 353 gives an abbreviated example and names "additional features keys such as …"; no page enumerates every key. No doc change owed. Agreed.
  8. objectui. No consumer there imports PUBLIC_AUTH_FEATURE_NAMES or PublicAuthFeatureSemantics (its features type at packages/auth/src/types.ts:282 is hand-written), so the widened enum cannot break the pinned console; the objectui half (Setup → Users copy, the reminder) is its own card under plugin-auth, setup users: a managed (SSO-only) hosted environment keeps only authorization + view in Setup → Users, turns its local sign-up off, and gives a JIT user the declared default permission set (cloud#3132 ruling A) #25083. Console Pin Gate was skipped by path filter; nothing here needed it.
  9. Lanes. The 跨域例外路径 is on record: claim amendment 6352529197 on spec: declare the console flag features.cloudManaged — a PUBLIC_AUTH_FEATURES entry with a hide-when-on gate semantics, and AuthFeaturesConfigSchema declares it (child of #25083, ruling 6341690661 Q3 A) #25292 and, per supplement 6353227863, claim 6353249248 on platform-objects: hide invite_user ×4, create_user and set_user_password when features.cloudManaged is on (visible predicates, hide-when-on) #25295 (branch claude/project-thread-4kg5av); The card this PR closes must claim this branch is success on this head. The PR title names spec and platform-objects only; the body names the plugin-auth line. Cosmetic.

The rest of the diff (the generated reference pages, api.json, the test-fixture updates) is the lane seat's ACCEPT at TIER_DEFAULT.

Implemented-by: claude/project-thread-4kg5av
Reviewed-by: session_0149wZgvyZCLDzZmjCgTrcgt

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot removed the needs:contract-review Marker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides label Oct 23, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:spec seat 2 (#18549) · os-bill · session session_01B2EeooonSXrANpkpgUSfFZ · 2026-10-23T02:08Z · head 72cea5b1ef8d645b9b5e004fd95037e39e021f09

  1. Shape: draft until the maintainer's 追认 (now recorded), base main, closing keywords Fixes #25292 and Fixes #25295, and Part of #25083 (never Fixes #25083), after the session's attribution block. One joint PR under ruling 6352436010 (letter A, the cross-domain exception path), supplement 6353227863.
  2. Premise measured: the spec entry, the six platform-objects predicates and plugin-auth's served line are held equal by three required pins, so they land in one commit. The spec half alone on the base reds 6 forward-guard rows and 2 plugin-auth registry cases (measured).
  3. Gates: check-runs on this head 43 success and 14 skipped, none failed; The card this PR closes must claim this branch green after the platform-objects: hide invite_user ×4, create_user and set_user_password when features.cloudManaged is on (visible predicates, hide-when-on) #25295 claim 6353249248. The dev's local dispatch-gates: 117 derived, 116 exit 0, 1 NOT-MEASURED (check-plugin-teardown-shape --self-test, a shallow clone cannot read its pinned fixture commit; CI ran it), 0 UNRUN.
  4. Contract review: PASS at CONTRACT_REVIEW_TIER on this head, record 6355339198 (another account's review session, as the maintainer arranged). Engine seat's read of the platform-objects hunks: PASS 6353213686 on d30caab47b; git diff d30caab4 72cea5b1 -- packages/platform-objects is empty, so it covers this head.
  5. Ruling: 6352436010 letter A, a proxy ruling, ratified by the maintainer 「spec: declare the console flag features.cloudManaged — a PUBLIC_AUTH_FEATURES entry with a hide-when-on gate semantics, and AuthFeaturesConfigSchema declares it (child of #25083, ruling 6341690661 Q3 A) #25292 追认 A」, recorded 6355684586.
  6. Accepted deviations and findings:
  7. Out of scope: sso.mdx's abbreviated features example needs no change (review ③7).
  8. Landing: pr_ready + automerge_enable through the relay now. The seat follows it to MERGED.

Generated by Claude Code

Merged via the queue into main with commit f4112a7 Oct 23, 2026
58 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-4kg5av branch October 23, 2026 02:57
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 28, 2026
…e console on a cloud-managed environment (objectui#13330) (objectstack-ai#13372)

<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuMRtFBARXxNvcE3v2DW5kAK)_

Fixes objectstack-ai#13330
Clause-②: yes
Metadata-protocol: none

The console half of the objectstack-ai/objectstack#25083 ruling (comment
6341690661, Q1 B / Q3 A / Q4 A). When `/auth/config` serves
`features.cloudManaged: true`, the console says what the server already
enforces. People are invited to the hosting organization in the cloud,
and the environment keeps authorization and view. When the flag is
absent or `false`, every surface below is unchanged, and each one has a
pinned control for that.

## What changes on a managed environment

| Surface | Flag `true` | Flag absent / `false` |
|---|---|---|
| Setup → Users list | One line above the list says where to invite
people. A user who holds no permission set beyond the baseline shows a
「未授权」 / "Not authorized" marker beside their name. | No line, no marker,
no grant reads. |
| Setup → Users identity import (`identityImportEnabled`) | Not offered.
The server answers `admin/import-users` with 409. | Offered to an admin,
as before. |
| Organization → Members | A note ("People join through your
organization in the cloud. Invite them there.") instead of Invite
member. The invite dialog is not mounted. | Invite member, as before.
This also holds while `/auth/config` has not answered, or cannot. |
| Recovery-password reminder | Not shown. | Unchanged. |

The `invite_user`, `create_user` and `set_user_password` actions are
hidden by their own served `visible` predicates (the engine lane's item
on objectstack-ai/objectstack#25083), not by console code.

The console reads the flag directly from `/auth/config` into its own
`AuthPublicConfig` type. So this PR does not wait for the
`.objectstack-sha` pin (`06cab48`, 18.0.0) to cover
objectstack-ai/objectstack#25309, which serves the flag.

## The marker: what "beyond the baseline" reads

Every signed-in user holds the baseline: the sets that the `everyone`
anchor position lists (the platform's `member_default` and the defaults
the environment accepted). A user holds something beyond the baseline
when a grant that is active now (`valid_from` / `valid_until`,
half-open, the window the server enforces) does either of these:
- a `sys_user_permission_set` row names a set that the anchor does not
list;
- a `sys_user_position` row names a position other than an anchor
(`everyone`, `guest`) whose registry definition lists such a set.

These are the same reads that the permission set's Assigned Users
section makes, asked by user instead of by set. One loader per list
batches every marker the page draws, so a page costs one read per
object: `$filter: { user_id: { $in: [...] } }`, 50 users per read,
`$top: 1000`. The loader is rebuilt when the list refreshes. Unknown
never counts as "not authorized": a refused read, an answer the server
reports as cut short (`hasMore`, or a `total` longer than the page), or
a position the registry does not define all leave the user unmarked.

## The new host prop: `rowAdornment`

`ListView` → `ObjectGrid` had no per-row slot a host could draw into.
Both packages already take host-only React props for this kind of need,
never schema keys: `rowOperations`, `objectFields` and `showRowNumbers`
on the grid, and `toolbarLeading` on the list. So the marker rides one
generic prop, named for what it draws (ADR-0060 rule 2):
- `ObjectGrid.rowAdornment?: (record) => ReactNode` is drawn after the
title cell's own content. That is the declared `nameField` column when a
column shows it, else the first shown column, never the row actions. In
the card view it sits beside the card title. The cell's value, link and
mask stay as drawn. A `null` return draws nothing, and an absent prop
renders exactly as before.
- `ListView.rowAdornment` is destructured by name and handed only to the
grid, in the same way as `showRowNumbers`. The board and the other
visualizations, and the compact posture, are not handed it.

The card's claim first planned the marker as a second PR behind a
ruling. Correction 6365253236 on the card records why it is in this PR
instead, and the revised file surface. If contract review would rather
take it separately, it comes out cleanly: the two props, their wiring in
`ObjectView.tsx`, the marker and their tests. Nothing else in this PR
depends on it.

## What changed

- `packages/auth/src/types.ts`:
`AuthPublicConfig['features'].cloudManaged`, documented beside
`ssoEnforced`. The auth README has a new `features.cloudManaged`
section.
- `packages/app-shell/src/views/managedEnvironmentUsers.tsx` (new):
`isManagedEnvironment`, `ManagedUsersInviteLine`, the grant read
(`readUserGrantStates`, `isGrantActive`), the batching
`UserGrantLoader`, and `UnauthorizedUserMarker`.
- `packages/app-shell/src/views/ObjectView.tsx`: the identity import
gate, the invite line on the `sys_user` list, and `rowAdornment` passed
to `ListView`. The metadata store is read through a ref, and the loader
lives in a ref keyed on the refresh key and the data source, so no
correctness rests on a memoised identity (AGENTS.md objectstack-ai#10).
- `packages/app-shell/src/console/organizations/manage/MembersPage.tsx`:
reads `/auth/config` itself, and the note takes the button's place (the
objectui#4475 convention). This is an edit to an existing file under the
frozen directory. No file is added there, apart from a test.
- `packages/app-shell/src/console/home/RecoveryPasswordReminder.tsx`:
skips the reminder when the flag is `true`.
- `packages/plugin-grid/src/ObjectGrid.tsx` and
`packages/plugin-list/src/ListView.tsx`: `rowAdornment`, with docblocks,
README sections, and the mirrored `content/docs/plugins/plugin-grid.mdx`
section.
- `packages/i18n/src/locales/*.ts` (all ten):
`console.objectView.managedUsers.{inviteLine,unauthorized,unauthorizedHint}`
and `organization.members.inviteManagedNote`. No key name leads with a
product word, and no frozen namespace grows.
- `.changeset/13330-managed-environment-users.md`: `@object-ui/auth`,
`@object-ui/plugin-grid` and `@object-ui/plugin-list` minor (a new
public member or prop each); `@object-ui/app-shell` and
`@object-ui/i18n` patch.

## Tests

New pins. Each case is paired with a control in which the flag is absent
or `false`, or the prop is absent:
- `ObjectView.managedEnvironmentUsers-13330.test.tsx` runs the real
`ObjectView` → `ListView` → `ObjectGrid` chain with the en bundle. It
checks that the invite line shows, and that the marker appears on a
baseline-only user and on a user whose only grant has expired, but not
on a user with a direct grant or a granting position. It checks that one
read per object is asked by user, and that identity import is not
offered. In the control, the line, the marker and the grant reads are
all absent, and identity import is offered.
- `__tests__/managedEnvironmentUsers-13330.test.tsx`: the flag reader,
the grant window bounds, a baseline set held explicitly, positions and
anchors, an undefined position, both shapes of a truncated answer, a
refused read, and the marker drawing nothing on a refused read.
- `RecoveryPasswordReminder.cloudManaged-13330.test.tsx`,
`members-invite-managed-13330.test.tsx`, `rowAdornment-13330.test.tsx`
(grid: name-field cell, first column, card title, control) and
`ListView.rowAdornment-13330.test.tsx` (grid handed it, board not,
control).

Ablation, run on the committed tree and then restored: each of these
reverts turns its pin red while the controls stay green. Dropping the
import gate, the members note, the reminder skip, the invite line, the
grid placement (table and card), the list forwarding, the name-field
preference, and the `valid_until` bound.

Run locally, on a box shared with other agents:
- `pnpm exec vitest run packages/plugin-grid packages/plugin-list
packages/auth packages/i18n packages/app-shell/src/views
packages/app-shell/src/console/organizations
packages/app-shell/src/console/home` plus the product-neutral ratchet:
1689 files and 16573 tests. The first run had 3 failures, in two i18n
pins that count live keys: `de-quote-pairing-3876` (paired „…“ spans, 91
→ 92) and `objectView-config-keys-retired-4730` (surviving
`console.objectView` keys, 104 → 105, total 228 → 229). Both are
advanced in the second commit with the same narration their histories
use, and both pass after merging `main`, along with
`all-locales-key-parity`, the ratchet and the end-to-end pin.
- `turbo run type-check` for `@object-ui/app-shell`, `plugin-grid`,
`plugin-list`, `auth` and `i18n`: all tasks successful. One error, in
this PR's own test double (an untyped `params` argument), was fixed in
the second commit.
- `pnpm check:doc-snippets` after its scoped build: 811 of 811 blocks
compile, including the new README and mdx snippets.
- `check:i18n-keys`, `check:i18n-drift` (4 keys added, 0 en values
changed), `check:i18n-dead-keys` (the new keys are not listed),
`check:new-line-citations` (0 new), `check:control-bytes`,
`check:doc-fences` and `check-changeset-presence`: all pass.
- NOT MEASURED live: a browser pass against a cloud-managed objectstack.
The pins above use the real component chain with served-shape fixtures.

- [x] Adds no product-specific widget, dialog, hook or string (see
AGENTS.md › Product-neutral renderer). The surfaces read a platform flag
that `@objectstack/spec` declares
(`AuthFeaturesConfigSchema.cloudManaged`). The new prop is generic and
named for what it draws. No file, registry key or locale key name leads
with a product word.

---
_Generated by [Claude
Code](https://claude.ai/code/session_0122QNQ7jXCC1tUKFY6YnZVo)_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_0122QNQ7jXCC1tUKFY6YnZVo

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants