Repository navigation
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
Conversation
…mantics (WIP) Claude-Session: https://claude.ai/code/session_01P9sxzbFDWjQD3FdsL3mN4u Co-authored-by: Claude <noreply@anthropic.com>
…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>
Claude-Session: https://claude.ai/code/session_01P9sxzbFDWjQD3FdsL3mN4u Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 148 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
|
engine seat 2 · platform-objects hunk · PASS · |
Claude-Session: https://claude.ai/code/session_01P9sxzbFDWjQD3FdsL3mN4u Co-authored-by: Claude <noreply@anthropic.com>
…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>
Contract reviewServed-tier: 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 Precondition of ruling A (#25294 on ① Derived judgmentsSpec half (the hits), each read against ruling 6341690661 Q3 A and ruling 6352436010 items 1–6:
Files the spec hunks make a claim about (a rule and its gate read as one):
② Semver level
③ Boundary flags
The rest of the diff (the generated reference pages, Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
…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>
Requested by Bill · project thread
Fixes #25292
Fixes #25295
Part of #25083
Clause-②: yes (widening)
Metadata-protocol: added
The console flag
features.cloudManagedfor the managed (cloud-hosted, SSO-only) mode, built as one trunk under ruling6352436010on #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 is6341690661on #25083, Q3 A (and Q4 A forset_user_password).Spec half (#25292)
PublicAuthFeatureSemanticsgains'hide-when-on': the flag being ON hides the input.featureGatePredicatelowers it tofeatures.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_useronsys_user,sys_member,sys_invitation,sys_organization;sys_user.actions.create_user;sys_user.actions.set_user_password. ThroughPUBLIC_AUTH_FEATURE_NAMES,requiresFeature: 'cloudManaged'is now accepted and lowers tofeatures.cloudManaged != true(this is theMetadata-protocol: added).AuthFeaturesConfigSchema.cloudManaged: z.boolean().optional(), the form of the newest siblings (ssoEnforced,phoneNumber,phoneNumberOtp).content/docs/references/{api/auth-endpoints,ui/action}.mdx, one line ofpackages/spec/authorable-surface/api.json(check:generated --fixproved onlycheck:docsstale;api-surfaceunchanged).content/docs/protocol/objectui/actions.mdxnames the third lowering.Engine half (#25295)
requiresFeature(organizationx4,adminx2) and gainsfeatures.cloudManaged != truewritten invisible, sincerequiresFeaturenames one flag. Lowered, e.g.(current_user.isPlatformAdmin == true && features.cloudManaged != true) && features.admin == true.set_user_passwordis hidden in the console only; no server-side refusal here (the server keeps answering it on anenv_nativerow).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: acceptedgate,(a) && gate,(a && gate) && band the invite shape; refuseda || gate,a || b && gate,!(gate),a ? gate : true, a longer token, and the gate inside a string literal.identity/cloud-managed-visibility.test.tsevaluates the six lowered predicates through@objectstack/formula'scelEngine: hidden withcloudManaged: true, offered withfalse, and still hidden when its other flag is off.platform-objects.test.ts(lowering matrix),invite-entry-toolbar.test.ts(the mirrors agree);platform-admin-affordance-standing.test.tsservescloudManaged: falsebeside the other flags.Services line (part of #25083)
getPublicConfig()'sfeaturesliteral (packages/plugins/plugin-auth/src/auth-manager.ts) gains exactly one line,cloudManaged: isCloudManaged(), withisCloudManagedadded to the existing@objectstack/coreimport. It reads the one accessor core: isCloudManaged() — one accessor for OS_CLOUD_MANAGED=1 beside isHostKernelComposition, with the production fuse (no bound control plane ⇒ refuse to start) #25294 landed, so the console's hide-when-on gate and every server-side managed-mode judgement read the same answer. Everything 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 stays on its own claim.auth-manager.test.ts: the exact-match payload test listscloudManaged: false(OS_CLOUD_MANAGEDunset), and a new case setsOS_CLOUD_MANAGED=1, resets the memoized verdict withresetCloudManagedForTesting(), readstrue, and restores the environment.Verification (head
72cea5b1)mainmerged 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 asdb1c7ccad.@objectstack/spec:check:generatedreports all 14 generated artifacts up to date after rebuilding spec on the merged tree. Spec suite ond30caab4: 783 files, 22794 passed, 1 todo;typecheck0.@objectstack/plugin-auth: fullvitest runon91d3cd6bwas 186/187 files, the one red being the exact-match payload test missingcloudManaged(fixed in72cea5b1); on72cea5b1auth-manager.test.ts+public-feature-registry.test.ts302/302 passed, so the registry drift guard is green;typecheck0.cloudManaged: isCloudManaged()withcloudManaged: falseon disk redsserves features.cloudManaged from OS_CLOUD_MANAGED(1 failed); file restored from a copy,git diffempty.@objectstack/platform-objects:vitest run88 files, 1318 passed;typecheck0.scripts/ablation-replace.mjs, restored to the HEAD blob each time: deleting the authored gate fromsys_member.invite_userredssys_member.actions.invite_user is gated on cloudManaged(1 failed / 100 passed); replacing it withtrue || features.cloudManaged != truereds the same row (1 failed / 101 passed).72cea5b1:dispatch-gates --commandslists 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).--ranreconciles 117 derived, 116 run, 1 NOT-MEASURED, 0 UNRUN.72cea5b1: 36 check runs success, 6 skipped, none red (Test Core 1–6, Build Core, Build Docs, every Lint and Type Check lane, andThe card this PR closes must claim this branch).Acceptance notes
default-onflag "fails open". In@objectstack/formula'scelEnginean absent key faults (No such key) for every flag,default-onincluded, 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.(narrowing)was looked for and not found: the six actions only hide where the server servescloudManaged: true, which happens only on a runtime withOS_CLOUD_MANAGED=1(the cloud-hosted one, whose people arrive through the identity provider); every other deployment servesfalseand sees no change.Changesets:
@objectstack/specminor (Clause-②: yes (widening),Metadata-protocol: added);@objectstack/platform-objectspatch (Clause-②: no,Metadata-protocol: none);@objectstack/plugin-authpatch (Clause-②: yes (widening),Metadata-protocol: none).Review:
CONTRACT_REVIEW_TIER(schema face) by thedomain:specseat; thedomain:engineseat posts one PASS/FAIL line on the platform-objects hunks. #25294 is onmain; draft until the maintainer has confirmed ruling6352436010.🤖 Generated with Claude Code
https://claude.ai/code/session_01P9sxzbFDWjQD3FdsL3mN4u
Generated by Claude Code