Skip to content

Commit c71d03a

Browse files
fix(cloud-connection)!: one rule for the capability — manage_platform_settings gates the record and the three doors; a managed runtime also refuses bind/start and bind/poll (#25143)
Fixes #25119 Fixes #25113 Clause-②: no (narrowing) Metadata-protocol: none ## What this does Ruling 乙 on #25119. The maintainer chose the option labelled 「乙 统一成一套」: the page and the three doors also require `manage_platform_settings`. One rule now covers the Cloud Connection capability: the Setup entry, the page's record and the three connection-changing doors. - **One capability gate.** `holdsSetupAccess` is now `holdsPlatformSettings`, the single reading of the grant on this surface. It reads `manage_platform_settings` (`CLOUD_CONNECTION_CAPABILITY`) from `resolveAuthzContext`'s `systemPermissions`, judged on the session the door already admitted. That is the shape `service-datasource`'s admin routes take for the same capability (#9593). It needs no new dependency edge, because the plugin already imports the resolver from `@objectstack/core`. All four call sites read it: `bind/start`, `bind/poll`, `unbind` and `status`'s full-record branch. - A caller without the capability gets `403 PERMISSION_DENIED` naming `manage_platform_settings`. The status and code are what `refuseWithoutSetupAccess` answered before (H4 holds); only the capability changed. - `status` still serves `bound`, `runtimeId` and `provider` to every caller, and `environmentId` and `connection` to holders only. - The organization administrator holds `setup.access` without `manage_platform_settings` in `plugin-security`'s defaults. It is now refused at every door and served the narrow status. - **Managed runtime (#25113).** On a runtime holding the service key, `bind/start` and `bind/poll` now refuse as `unbind` does: `409 RESOURCE_CONFLICT`, after the identity and capability checks and before any control-plane call or store write. - One refusal constant (`PLATFORM_MANAGED_REFUSAL`) and one helper (`refusePlatformManaged`) serve all three doors. - The message's first sentence is unchanged: "This runtime's cloud connection is managed by the platform". It now says the connection cannot be connected or disconnected here, and that nothing was sent to the cloud and nothing was stored or cleared. - **The entry declares the rule on itself.** `nav_cloud_connection` now carries `requiredPermissions: [CLOUD_CONNECTION_CAPABILITY]`. Its group already required this capability; now the entry and the doors read one constant from this package. `nav_datasources` in the same group follows the same shape. ## The page: measured, not declared (H2 disproven) Pages carry no `requiredPermissions` key. `PageSchema` (`packages/spec/src/ui/page.zod.ts`) is a strict object that declares none. Its own guidance for `permissions` says: "a page is not permission-gated by a field — gate the DATA it shows". The meta read gate (`packages/rest/src/meta-item-read-gate.ts`) has arms for `app`, `dashboard`, `view`, `object`, `book` and `doc`, and none for `page`. Writing the key would fail `tsc` (the page is typed `Page`) and the strict parse, and `packages/spec/**` is out of scope. So no key was invented. How the page is gated today: - **Entry:** pruned server-side by `group_integrations`' `requiredPermissions: ['manage_platform_settings']`. - **Page metadata:** `cloud_connection_settings` is a static shell (a header plus the `cloud-connection:panel` widget), and no read gate covers it. - **What it shows:** everything comes through this plugin's routes. The panel reads `GET status` and drives the three doors. With this PR, the page is held at the capability line where its data is served. A non-holder who opens the page by its address sees only the narrow status. Their Connect or Disconnect click is refused with the server's message, which the panel shows verbatim (objectui `CloudConnectionPanel.tsx` maps no translation for `PERMISSION_DENIED`). I did not add a `visibleWhen` on the panel. It would be a second, client-side reading of the grant, which the card rules out ("one resolver, no second reading of the grant"). It would also turn a refusal that names the capability into a blank page. ## Hypotheses, measured - **H1, holds.** At `1b3b65e643`, `holdsSetupAccess` was at `:516`, `refuseWithoutSetupAccess` at `:551`, and the full-record branch at `:579`. - **H2, disproven.** See the section above. - **H3, holds.** The resolver is `resolveAuthzContext` from `@objectstack/core`, already imported, so there is no new dependency edge. `DATASOURCE_ADMIN_CAPABILITY` lives in `service-datasource`, which this package does not depend on, so the capability gets a local constant. The datasource family uses a local literal too. - **H4, holds.** `403` / `PERMISSION_DENIED`, unchanged. - **H5, disproven.** `cloud-connection-route-ledger.ts` records no gate for the four binding rows (status, bind/start, bind/poll, unbind), only their disposition and a note. Nothing moves there. ## Pins, updated and not loosened - **`cloud-connection-setup-access.pin.test.ts`** (55 cases). The file keeps its name; its subject is now "`setup.access` alone does not pass". - Each door refuses two callers, under both modes and both connection shapes: a member, and an organization administrator holding the default `organization_admin` capabilities. That is one case per door per caller. - Every refusal asserts the status, the `code`, the envelope, a message naming `manage_platform_settings`, and a message NOT naming `setup.access`. - Holders: a platform admin, and an org-scoped `platform_operator` grant. They are served as before on a self-hosted runtime, and meet the managed `409` at every door on a managed runtime. - Both non-holders get the narrow `status` (exact object). The key-beside-session cases run for both non-holders. - A new case pins that the nav entry declares exactly `['manage_platform_settings']`. - **`cloud-connection-platform-managed.pin.test.ts`.** All three doors, both modes: managed gives `409 RESOURCE_CONFLICT`, the envelope, the message's first sentence, zero control-plane calls, and the stored credential neither replaced nor cleared. The not-managed control drives bind/start, then bind/poll (persists), then unbind (revokes and clears), with the exact call sequence. The registration control asserts the served entry's `requiredPermissions`. - **`cloud-connection-auth-gate.pin.test.ts`.** The fixture's `admin_full_access` carries `manage_platform_settings`. In the managed UN-HELD case, four doors are served, and bind/start, bind/poll and unbind are each the managed `409`, not the gate. - **`error-envelope.conformance.test.ts`.** The 403 case now uses a session holding `setup.access` without `manage_platform_settings`. New: bind/start and bind/poll on a managed runtime give `409` in the declared envelope. - **Fixtures whose subject is not the permission** (`cloud-connection-plugin.test.ts`, `connection-credential-store.test.ts`, `cloud-connection-runtime-version.pin.test.ts`): the admin's set now carries `manage_platform_settings`. Repo-wide sweep for pins of the same semantics: a `git grep` for the refusal message, `setup.access` near cloud-connection, and the old symbols outside `packages/cloud-connection` found no code pin. objectui's `CloudConnectionPanel` tests pin neither capability. The docs page is updated (below). ## Ablations (one per behaviour; the subject is imported from `src/`, so no dist leg) - **Capability read reverted to `'setup.access'` in `holdsPlatformSettings`.** Anchor 1 to 0, blob `4637329150d5` to `c6d2e5143a67`. The setup-access pin plus the envelope conformance gave **21 failed, 62 passed**. Restored: blob equals HEAD (`4637329150d5`) and `git diff HEAD` is empty. - **bind/poll's managed refusal deleted.** Anchor 1 to 0, blob `4637329150d5` to `75073de2f440`. Four suites gave **6 failed, 102 passed**: bind/poll in the managed pin (both modes), the auth-gate managed case, the setup-access managed control (both modes), and the envelope bind/poll `409`. Restored byte-identical. The first attempt at this ablation was a no-op that `ablation-replace` refused: the replacement was a substring of the anchor, so the count did not rise, and no test ran. It was redone with `--delete`. ## Changeset level: `patch`, not the card's `minor` The card said "`minor` with the BREAKING banner per AGENTS.md". AGENTS.md (Post-Task Checklist 3) and the `Check Changeset` gate now follow ruling A of 2026-10-21 on #25105: the level follows the metadata protocol alone. This diff changes which callers the routes serve, and no authorable key or value, so it is `Metadata-protocol: none`. That means `patch`, keeping the **BREAKING** banner and the ADR-0087 disposition (`not-required (no-migration-prescription)`). A `minor` with `none` is refused by `check-changeset-no-major`. ## Docs `content/docs/deployment/cloud-connection.mdx`: - "The right people on each side" now states the one rule: the entry, the requests and the details. It says `setup.access` alone is not enough, and that the default organization administrator cannot connect or disconnect. - The failure table gains the capability message. - The Reference section names the capability, and the managed `409` for all three doors. - The service-key paragraphs say "connect or disconnect". No other page names `setup.access` as this capability's gate. `setup-app.mdx` names only the group. - `.changeset/25097-cloud-connection-platform-managed.md` (the pending note from the platform-managed change): its sentences now match this PR (see Acceptance notes). ## Verification (head `d0bc81613`) - `pnpm --filter @objectstack/cloud-connection test`: **54 files, 670 tests passed**. `typecheck` (both programs) is clean, and the test program includes 54 of the package's test files (`--listFiles`). - Derived gates (`dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`): **96 derived, 96 run, 0 NOT-MEASURED, 0 UNRUN** (`--ran` exit 0). Three first refused with exit 3 (prerequisite not met), and each was re-run green once its prerequisite was met: - `check-plugin-teardown-shape --self-test` needed fixture commit `621a4876`, fetched by sha. - `check:skill-examples` and `check:dual-build-cjs-loads` needed built dist. A full `pnpm build` took 75 of 76 turbo cache hits. - `check-platform-checklist --changed-from origin/main`: OK. - `eslint --no-inline-config` over the nine changed TypeScript files: exit 0. The full `pnpm lint` is CI's run. - Dogfood battery at this head: reported in the issue's report comment. ## Acceptance notes - **Pending release note corrected (deliberate correction, ordered by the seat).** The pending 25097 changeset's two sentences now match this PR: "after the sign-in and Setup Access checks" now reads "after the sign-in and capability checks", and "The connect and disconnect requests still take Setup Access alone." is dropped. Its Unchanged bullet now names the permission this release raises to `manage_platform_settings`. The frontmatter is untouched. `Check Changeset` stays red on its foreign-changeset step by design (check-empty-changeset, deliberate-correction class); its ADR-0087 and level steps are green. This line is the written confirmation that route asks for. - **Page metadata.** The `cloud_connection_settings` page metadata itself remains readable through the meta routes by any authenticated caller, as every page is: no page read gate exists. It carries only a title, a subtitle and the widget type. - **Untouched routes.** `POST /install` and the three read routes still take any signed-in session (pre-existing, with a TODO in the code; out of scope per the card). Noted, not filed: no public-door reading was taken. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01AFrKwndmrCLtF5V9hNtrw3)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 37ccd5f commit c71d03a

12 files changed

Lines changed: 551 additions & 324 deletions

‎.changeset/25097-cloud-connection-platform-managed.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ fix(cloud-connection)!: a runtime holding the environment service key is platfor
66

77
A runtime whose control-plane credential is the environment service key (`controlPlaneApiKey`, or `OS_CLOUD_API_KEY`, which the cloud sets on the environments it hosts) is connected by the platform, not by anyone on the runtime. It still registered the Cloud Connection page in Setup's Apps group, which read "Connected" and offered Disconnect. Disconnecting there cleared the runtime's stored device credential while the service key stayed in use, so the page went on reading "Connected", and the control plane was asked to revoke a connection the platform had made.
88

9-
Such a runtime now registers neither the page nor its navigation entry, and says so once at boot. A request to disconnect it is refused with `409` and `RESOURCE_CONFLICT`, after the sign-in and Setup Access checks and before anything is revoked or cleared. The message says the platform manages the connection.
9+
Such a runtime now registers neither the page nor its navigation entry, and says so once at boot. A request to disconnect it is refused with `409` and `RESOURCE_CONFLICT`, after the sign-in and capability checks and before anything is revoked or cleared. The message says the platform manages the connection.
1010

11-
A self-hosted or local runtime, connected through the device-code flow, keeps the page and its disconnect unchanged. Its Cloud Connection entry moves from the Apps group to the Integrations group, after Webhooks, Datasources and Connect an Agent. That group requires `manage_platform_settings`, so a user who holds Setup Access without it, such as an organization administrator, no longer sees the entry. The connect and disconnect requests still take Setup Access alone.
11+
A self-hosted or local runtime, connected through the device-code flow, keeps the page and its disconnect unchanged. Its Cloud Connection entry moves from the Apps group to the Integrations group, after Webhooks, Datasources and Connect an Agent. That group requires `manage_platform_settings`, so a user who holds Setup Access without it, such as an organization administrator, no longer sees the entry.
1212

1313
Clause-②: no (narrowing)
1414

1515
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is what a runtime holding the environment service key serves: it no longer registers the Cloud Connection page or its navigation entry, and it refuses a disconnect it used to carry out. The navigation entry's new group is a value inside an exported constant whose type is unchanged. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and nothing exported is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
1616

1717
**BREAKING** (on a runtime holding the environment service key, a disconnect request it carried out is now refused, and the Cloud Connection page and its navigation entry are no longer served), shipped as `minor` under the launch-window convention for breaking changes.
1818

19-
- **Unchanged:** a runtime without the service key, apart from the entry's group. The connection status answer is unchanged on every runtime.
19+
- **Unchanged:** a runtime without the service key, apart from the entry's group and the permission its connect and disconnect requests and its connection details require, which another change in this release raises to `manage_platform_settings`. The connection status answer is otherwise unchanged on every runtime.
2020
- **Nothing to rewrite:** a cloud-hosted environment's connection is the platform's to manage. A self-hosted server leaves `OS_CLOUD_API_KEY` unset, as the deployment guide says; one that sets it is platform-managed by this rule.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/cloud-connection': patch
3+
---
4+
5+
fix(cloud-connection)!: one rule for the Cloud Connection capability — the page's record and its three connection-changing requests require `manage_platform_settings`, like its Setup entry; a server holding the environment service key also refuses `bind/start` and `bind/poll`
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: no spec key, authorable spelling, object definition or stored shape is added, removed, renamed or re-shaped, and no exported declaration is removed or narrowed, so there is nothing for `objectstack migrate meta` to rewrite. What changes is which signed-in callers the Cloud Connection routes serve: the capability they read moves from `setup.access` to `manage_platform_settings`, and a server holding the environment service key refuses two more of them. The navigation entry's added `requiredPermissions` is a value inside an exported constant whose type is unchanged. -->
11+
12+
**BREAKING** (a signed-in user who holds Setup Access, `setup.access`, without Manage Platform Settings, `manage_platform_settings`, such as an organization administrator under the default permission sets, can no longer connect or disconnect the server, and is no longer served the connection's details), shipped as `patch`: the change is to which callers the routes serve, not to the metadata protocol.
13+
14+
The Cloud Connection page's Setup entry sits in the Integrations group, which requires `manage_platform_settings`. The page's routes still took `setup.access` alone, so an organization administrator, whose default permission set carries Setup Access but not Manage Platform Settings, was not shown the entry but could open the page by its address and connect or disconnect the server. One rule now covers the entry, the page and the routes:
15+
16+
- **Refused now:** `POST /api/v1/cloud-connection/bind/start`, `POST /api/v1/cloud-connection/bind/poll` and `POST /api/v1/cloud-connection/unbind` answer a signed-in user who does not hold `manage_platform_settings` with `403` and the standard `PERMISSION_DENIED` code, and the message names that capability. Holding `setup.access` is no longer enough. The refusal still comes before any call to the cloud and before the stored credential is touched.
17+
- **Narrowed now:** `GET /api/v1/cloud-connection/status` serves `environmentId` and `connection` only to a user who holds `manage_platform_settings`. Every other caller, an organization administrator included, is served `bound`, `runtimeId` and `provider`, as an anonymous caller is.
18+
- **Declared on the entry:** the Cloud Connection navigation entry now declares `requiredPermissions: ['manage_platform_settings']` itself, the capability its group already required, so the entry and the routes read one rule from this package. The page carries no permission key of its own (a page has none); what it shows is served by these routes.
19+
- **Refused on a platform-managed server:** a server holding the environment service key (`controlPlaneApiKey`, or `OS_CLOUD_API_KEY`) already refused `unbind` with `409` and `RESOURCE_CONFLICT`. It now refuses `bind/start` and `bind/poll` the same way, after the sign-in and capability checks and before any call to the cloud or any write to the stored credential. A device connection completed there used to store a credential the service key always overrode, while presenting that key to the cloud, which could move the environment's cloud-side connection record. The message now says the connection cannot be connected or disconnected here, and that nothing was sent to the cloud and nothing was stored or cleared.
20+
- **Unchanged:** a user who holds `manage_platform_settings`, which the default platform-administrator permission set carries, is served exactly as before on a server without the service key. Anonymous callers still get `401`, and a session the authentication policy is holding still gets its `403`. The device-code flow's steps, the status answer for a permitted caller, and the four other session routes (`install`, `installation`, `installed`, `org-packages`) are unchanged.
21+
22+
**Who acts:** a deployment that relies on organization administrators, or another set holding only Setup Access, to connect its server grants `manage_platform_settings` to those users, through a permission set or a position, and they are served as before.

‎content/docs/deployment/cloud-connection.mdx‎

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@ needs no sign-in, and a `200` means the plugin is mounted.
5959
**The server does not hold the environment service key.** A server that holds
6060
`OS_CLOUD_API_KEY` (the cloud sets it on every environment it hosts) is
6161
connected by the platform, not from Setup. It registers no Cloud Connection page
62-
or Setup entry, says so once in its boot log, and refuses a disconnect with
63-
`409` (`RESOURCE_CONFLICT`): nothing is revoked and nothing is cleared. A
64-
self-hosted server leaves `OS_CLOUD_API_KEY` unset.
62+
or Setup entry, says so once in its boot log, and refuses the connect and
63+
disconnect requests with `409` (`RESOURCE_CONFLICT`): nothing is sent to the
64+
cloud, and nothing is stored or cleared. A self-hosted server leaves
65+
`OS_CLOUD_API_KEY` unset.
6566

6667
**The server can reach the cloud.** Every call to the cloud is made by the
6768
server, not by your browser: the code request, the polling, the connection
@@ -78,18 +79,28 @@ the server reads as not connected until you connect it again.
7879

7980
**The right people on each side.**
8081

81-
- *On the server*, you sign in with a user of this server. The Cloud
82-
Connection page is in the Setup app, which takes the **Setup Access**
83-
permission (`setup.access`); the default platform-administrator and
84-
organization-administrator permission sets carry it. The connect and
85-
disconnect requests take the same permission: a signed-in user without
86-
Setup Access is refused with `403` (`PERMISSION_DENIED`) before the server
87-
calls the cloud. The page's Setup entry sits in the **Integrations** group,
88-
which Setup shows only to users who also hold `manage_platform_settings`; the
89-
default platform-administrator permission set carries both. A session
90-
that the authentication policy is holding (an expired password, or
91-
multi-factor sign-in required but not yet set up) is refused with `403`
92-
until it is fixed.
82+
- *On the server*, you sign in with a user of this server who holds the
83+
**Manage Platform Settings** permission (`manage_platform_settings`); the
84+
default platform-administrator permission set carries it. One rule covers
85+
the whole Cloud Connection page:
86+
- Its Setup entry sits in the **Integrations** group, which Setup shows only
87+
to users who hold that permission.
88+
- The connect and disconnect requests take the same permission: a signed-in
89+
user without it is refused with `403` (`PERMISSION_DENIED`) before the
90+
server calls the cloud.
91+
- The connection's details (the approving account, the organization, the
92+
environment) are served only to users who hold it. Anyone else who opens
93+
the page by its address is shown only whether the server is connected and
94+
its runtime id, and the panel's **Connect** or **Disconnect** button is
95+
refused with the message listed under [Connect](#connect).
96+
97+
Setup Access (`setup.access`) alone is not enough. The default
98+
organization-administrator permission set carries Setup Access but not
99+
Manage Platform Settings, so an organization administrator can open Setup
100+
but cannot connect or disconnect the server. A session that the
101+
authentication policy is holding (an expired password, or multi-factor
102+
sign-in required but not yet set up) is refused with `403` until it is
103+
fixed.
93104
- *In the cloud*, the approval is made by whoever signs in to the cloud
94105
account behind the link and approves the request. The server records that
95106
account's email as **Approved by**. Which cloud accounts may approve, and
@@ -135,6 +146,7 @@ button:
135146
| *The connection request was denied. Start again.* | The cloud answered `access_denied`. |
136147
| *Device authorization failed: …* | The cloud ended the request with another error, named after the colon. |
137148
| *Sign in to connect a cloud account.* (or *Sign in.* while waiting) | Your session on the server has ended. |
149+
| *Managing this runtime's cloud connection requires the `manage_platform_settings` capability.* | You are signed in without Manage Platform Settings (see [Before you start](#before-you-start)). |
138150
| *No cloud control plane configured.* | The plugin is mounted without a control-plane URL. |
139151
| a network error such as *fetch failed* | The server could not reach the cloud. |
140152

@@ -216,14 +228,19 @@ account afterwards.
216228

217229
## Reference [#reference]
218230

219-
The routes the Cloud Connection page uses, all on the server's own origin:
231+
The routes the Cloud Connection page uses, all on the server's own origin. The
232+
three that change the connection (`bind/start`, `bind/poll` and `unbind`)
233+
require Manage Platform Settings (`manage_platform_settings`) and answer `403`
234+
(`PERMISSION_DENIED`) to a signed-in user without it. On a server holding
235+
`OS_CLOUD_API_KEY`, the same three answer `409` (`RESOURCE_CONFLICT`) to a user
236+
who holds it, and nothing changes.
220237

221238
| Route | Sign-in | What it does |
222239
|:---|:---|:---|
223-
| `GET /api/v1/cloud-connection/status` | not required | Every caller gets `bound`, `runtimeId` and `provider`. A signed-in user with Setup Access (`setup.access`) also gets `environmentId` and `connection`, the connection as the cloud reports it (the approving account and the organization). `bound` is `true` whenever the server holds a credential; `connection` is `null` when the cloud cannot be reached. |
240+
| `GET /api/v1/cloud-connection/status` | not required | Every caller gets `bound`, `runtimeId` and `provider`. A signed-in user with Manage Platform Settings (`manage_platform_settings`) also gets `environmentId` and `connection`, the connection as the cloud reports it (the approving account and the organization). `bound` is `true` whenever the server holds a credential; `connection` is `null` when the cloud cannot be reached. |
224241
| `POST /api/v1/cloud-connection/bind/start` | required | Asks the cloud for a device code; answers with `user_code`, `verification_uri`, `verification_uri_complete`, `interval`, `expires_in`. |
225242
| `POST /api/v1/cloud-connection/bind/poll` | required | Body `{ "device_code": "…" }`. Answers `{ "pending": true }` while the cloud waits for approval, and the connection once it is approved. |
226-
| `POST /api/v1/cloud-connection/unbind` | required | Disconnects; answers `{ "revoked", "cleared" }`. On a server holding `OS_CLOUD_API_KEY`, refused with `409` (`RESOURCE_CONFLICT`) and nothing changes. |
243+
| `POST /api/v1/cloud-connection/unbind` | required | Disconnects; answers `{ "revoked", "cleared" }`. |
227244

228245
The settings that shape the flow are listed in
229246
[Environment variables](/docs/deployment/environment-variables#multi-tenant--cloud-control-plane):
@@ -232,7 +249,8 @@ id the server presents (default `objectstack-cli`), and `OS_RUNTIME_VERSION`
232249
sets the version it reports. `OS_CLOUD_API_KEY` and `OS_ENVIRONMENT_ID` belong
233250
to cloud-hosted runtimes; a self-hosted server leaves both unset. A set
234251
`OS_CLOUD_API_KEY` is used instead of the stored credential, and makes the
235-
connection platform-managed: no Cloud Connection page, and no disconnect.
252+
connection platform-managed: no Cloud Connection page, and no connect or
253+
disconnect.
236254

237255
The Setup app itself, and how plugins add entries to it, is described in
238256
[Setup app](/docs/ui/setup-app). Running the server is covered in

0 commit comments

Comments
 (0)