Skip to content

Commit ae58b26

Browse files
fix(cloud-connection)!: a runtime holding the env service key is platform-managed: no Setup page, disconnect refused; the entry moves to Integrations (#25110)
Fixes #25097 Clause-②: no (narrowing) Ruling 甲 (the maintainer, in the triage chat, as the card records it: 「一、云连接 甲」), the platform half. The pair objectstack-ai/cloud#3137 carries the hosted per-env kernel's own registration and is not touched here. ## What changes A runtime is **platform-managed** when its effective control-plane credential is the environment service key (`controlPlaneApiKey`, or `OS_CLOUD_API_KEY`, which the cloud sets on the environments it hosts). `CloudConnectionPlugin` now reads that as one predicate, `platformManaged`, and the existing credential resolver is written in terms of it, so the two cannot disagree. It is used in two places: - **Registration.** A managed runtime does not register the Cloud Connection UI bundle, so neither the `cloud_connection_settings` page nor the `nav_cloud_connection` entry is served. It logs one `info` line at boot saying so. - **Disconnect.** A managed runtime refuses a disconnect with `409` `RESOURCE_CONFLICT`. The refusal comes after the sign-in, held-session and Setup Access checks, which keep their answers and their order, and before the control-plane revoke and the local credential clear. The message names the platform as the connection's manager and offers no remedy, because the runtime has none to offer. Everywhere else (self-hosted or local, connected through the device-code flow), the page and disconnect are unchanged. The nav entry moves from `group_apps` to `group_integrations`. It keeps priority 200, which sorts it last there. The other entries' priorities, read where they are declared: Webhooks and HTTP Deliveries (`plugin-webhooks`) 100, Datasources (`service-datasource`) 100, Connect an Agent (`mcp`) 110. Measured once on a merged Setup app with Cloud Connection registered first: `group_integrations` served `nav_webhooks, nav_http_deliveries, nav_datasources, nav_connect_agent, nav_cloud_connection`. **Error code, and the precedent followed.** `RESOURCE_CONFLICT` / 409 is the standard catalog's 409 member. No code is minted, on the rule this plugin's own `SETUP_ACCESS_REFUSAL` (`PERMISSION_DENIED`) already follows: the ADR-0112 ledger sends a generic condition to the standard catalog rather than registering a synonym. It is also the answer this package's install-local door gives for a refusal that comes from runtime state rather than from the request (the writable-base namespace conflict). A dedicated code would need a ledger entry under `packages/spec`, which is out of this lane. `error-envelope.conformance.test.ts` gains the case. ## Contract Every input whose answer changes, before and after. Nothing else moves: the connection status answer is unchanged on every runtime, and bind start and bind poll are unchanged. | Input | Before | After | |:--|:--|:--| | A disconnect request on a runtime holding the service key, from a signed-in caller holding Setup Access | The control plane was asked to revoke the connection. The stored device credential was cleared. The answer was success, and status went on reading bound, because the key stayed in use. | Refused: `409` `RESOURCE_CONFLICT` in the declared envelope, with a message naming the platform as the connection's manager. Nothing is revoked or cleared. | | The same request from a caller with no session, a held session or no Setup Access | 401, the gate's 403 or `PERMISSION_DENIED` 403 | Unchanged, and still answered first | | The page listing on a runtime holding the service key | serves `cloud_connection_settings` | does not serve it | | Setup's merged navigation on a runtime holding the service key | `nav_cloud_connection` in `group_apps` | no `nav_cloud_connection` anywhere | | Setup's merged navigation on a runtime without the key | `nav_cloud_connection` in `group_apps`, an ungated group | `nav_cloud_connection` in `group_integrations`, last. That group requires `manage_platform_settings`, so a caller holding Setup Access without it (the default `organization_admin` set) no longer sees the entry. The connect and disconnect requests still take Setup Access alone. | | The boot log of a runtime holding the service key | nothing about the page | one `info` line: platform-managed, page and entry not registered, disconnecting refused | | Exported `CLOUD_CONNECTION_NAV_CONTRIBUTIONS` / `CLOUD_CONNECTION_UI_BUNDLE` | `group: 'group_apps'` | `group: 'group_integrations'` (same type, same priority, same item) | The claim's Clause-② basis said the nav move "only regroups an entry". As measured, it also narrows who is served the entry on a runtime without the key: that is the fifth row. The row narrows a served answer, so the declaration stays `no (narrowing)`. **Changeset level.** The card says `patch`. The changeset grades `minor`. AGENTS.md (Post-Task Checklist step 3) reads "`(narrowing)` is BREAKING". The header of `scripts/check-changeset-no-major.mjs` reads "During the launch window we ship breaking changes as `minor`". All 19 pending changesets that carry `Clause-②: no (narrowing)` grade their primary package `minor`. The fixed group already holds pending `minor` stock, so the next release's version number does not move. The changeset carries the Clause-② line, a **BREAKING** banner, and an ADR-0087 disposition of not-required (no-migration-prescription). The level axis stands down on a `no` declaration. ## Tests New pin: `packages/cloud-connection/src/cloud-connection-platform-managed.pin.test.ts`, 8 cases. - Registration, read back from a real `ObjectKernel` + `ObjectQLPlugin` through the metadata protocol. Managed through `controlPlaneApiKey` and through `OS_CLOUD_API_KEY`: no page, and no `nav_cloud_connection` anywhere in Setup's tree. Not managed: the page is served and the entry is the only child of `group_integrations`, absent from `group_apps`. Each reading has a lit control in the same composition: a control page that IS served, and `nav_marketplace_browse` in `group_apps`. - Boot log: exactly one line naming the platform-managed state. - Disconnect, both resolution modes. Managed: `409` `RESOURCE_CONFLICT`, `BaseResponseSchema` + `envelopeViolations` + `ApiErrorSchema` all clean, and the message's first sentence pinned. The control plane gets zero calls, so no revoke, and the stored credential is untouched. Control, not managed: exactly one call, the revoke, and the credential is cleared. **Red then green.** Run against `origin/main`'s plugin (`5050442bb`): 6 failed, 2 passed (the two not-managed disconnect controls). After the fix: 8 passed. Hash-equal ablation at `adac487886`: both source files restored to their `5050442bb` blobs (`233061ff…`, `d84ed969…`, landing verified by `git hash-object`), then 5 suites run. Result: 10 failed, 69 passed, and every failure is a new assertion. Restored to the HEAD blobs (`be039a06…`, `beffc3d7…`) by `git checkout HEAD --`, with `git diff HEAD` empty and status clean. **Existing pins, what each asserts and what changed:** - `cloud-connection-auth-gate.pin.test.ts`: the UN-HELD control ("the same caller is served by all seven doors") ran on the service-key mount, where disconnect is now the 409. It now reads both shapes: self-hosted (all seven served, disconnect driven last because it really clears the credential the other doors present), and managed (six served, disconnect is the 409 with no control-plane call). HELD cases are unchanged. - `cloud-connection-setup-access.pin.test.ts`: the CONTROL ("a setup.access holder is served by all three doors") likewise reads both shapes. Bind start and bind poll keep every prior assertion in both. Disconnect is served in the self-hosted shape, and in the managed shape it is the 409 with the credential kept. The 403 refusals are unchanged and still run on the managed mount, so they still prove the 403 precedes the 409. - `marketplace-installed-ui-entry.pin.test.ts`: its "plugin absent" control used `nav_cloud_connection` as the plugin-contributed entry in `group_apps`. That entry left the group, so the control is now `nav_marketplace_browse` (the browse bundle is registered in the composition), and the shell gains the `group_integrations` anchor. The page-listing control (`cloud_connection_settings` served) is unchanged. - `packages/cli/test/platform-page-i18n-parity.test.ts` (plugin page i18n parity, read from the static bundle), `packages/cli/scripts/check-app-nav-i18n.mjs` (every merged Setup/Account nav id labelled in every locale), `packages/qa/dogfood/test/platform-app-object-entry-views.test.ts` (object entries' first list view): unchanged and green. Translation keys are per app and nav id (`apps.setup.navigation.nav_cloud_connection`), so the regroup changes none: `check:app-nav-i18n` OK, 12 contributors, setup 52 merged ids. At `b5445185dd` (the final commit): `pnpm --filter @objectstack/cloud-connection exec vitest run`, 54 files / 637 tests passed. `typecheck` exit 0. The three cross-package pins above were re-run green at the same commit (27, 49, and `check:app-nav-i18n` OK). ## Docs that the change would otherwise falsify - `content/docs/deployment/cloud-connection.mdx`: Setup, Integrations, Cloud Connection paths; a managed server has no entry and refuses to disconnect; the entry's group gate; the reference row for the disconnect request. - `content/docs/ui/setup-app.mdx`: the Apps and Integrations rows. - `docs/qa/platform-checklist/areas/platform-core.json`: `platform-core.marketplace-console-honesty` steps[3] and acceptance[0] re-pointed, revision 6. ## Acceptance notes Noted, not filed: - On a managed runtime, bind start and bind poll still run. A completed device bind there would persist a device credential that is never presented, because the service key wins. The binding flow is held off for this card, and with no page, nothing in the product reaches those requests on a managed runtime. Carrier: none. - `content/docs/ui/setup-app.mdx` was already missing Datasources (`service-datasource`) and Connect an Agent (`mcp`) from the Integrations row. Pre-existing doc drift, not widened here. Carrier: none. - Measured on `origin/main` (Zone 2 of the dispatch). Status reads bound on a runtime holding the key, both when the control plane answers not-bound and when it is down. A disconnect there asked the control plane to revoke the connection, emptied the stored token, and status still read bound afterwards. ## Gates Read at `b5445185dd` (the final commit). `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 97 commands. Each was run with its exit code captured before any pipe: all 97 exit 0. The 10 families under "Declared WIDE population" were also run: all exit 0. Reconciliation: "Run reconciliation — 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN." `pnpm lint` (the whole-repo `eslint . --no-inline-config`): exit 0 at `b5445185dd`, no output. It ran with a 6 GB heap rather than 4 GB, to give the whole-repo scan headroom. --- _Generated by [Claude Code](https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 03c12a5 commit ae58b26

11 files changed

Lines changed: 608 additions & 57 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/cloud-connection': minor
3+
---
4+
5+
fix(cloud-connection)!: a runtime holding the environment service key is platform-managed, so it registers no Cloud Connection Setup page and refuses to disconnect; everywhere else the Cloud Connection entry moves to Setup's Integrations group
6+
7+
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.
8+
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.
10+
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.
12+
13+
Clause-②: no (narrowing)
14+
15+
<!-- 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). -->
16+
17+
**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.
18+
19+
- **Unchanged:** a runtime without the service key, apart from the entry's group. The connection status answer is unchanged on every runtime.
20+
- **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.

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

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ description: Connect your own ObjectStack server to an ObjectStack Cloud account
55
---
66

77
A self-hosted ObjectStack server can be connected to an ObjectStack Cloud
8-
account. You start the connection on your own server, in **Setup → Apps →
9-
Cloud Connection**, and approve it in the cloud account with a one-time code.
10-
Nothing is typed into the server: no id, no key, no password. Under the hood
8+
account. You start the connection on your own server, in **Setup →
9+
Integrations → Cloud Connection**, and approve it in the cloud account with a
10+
one-time code. Nothing is typed into the server: no id, no key, no password. Under the hood
1111
this is the OAuth 2.0 device authorization grant
1212
([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)), run by the
1313
`@objectstack/cloud-connection` package on your server.
@@ -56,6 +56,13 @@ const cloudUrl = resolveCloudUrl(); // OS_CLOUD_URL; '' when declared off
5656
To check a running server, request `GET /api/v1/cloud-connection/status`. It
5757
needs no sign-in, and a `200` means the plugin is mounted.
5858

59+
**The server does not hold the environment service key.** A server that holds
60+
`OS_CLOUD_API_KEY` (the cloud sets it on every environment it hosts) is
61+
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.
65+
5966
**The server can reach the cloud.** Every call to the cloud is made by the
6067
server, not by your browser: the code request, the polling, the connection
6168
itself and the disconnect. The server needs outbound HTTPS to the control-plane
@@ -77,7 +84,9 @@ the server reads as not connected until you connect it again.
7784
organization-administrator permission sets carry it. The connect and
7885
disconnect requests take the same permission: a signed-in user without
7986
Setup Access is refused with `403` (`PERMISSION_DENIED`) before the server
80-
calls the cloud. A session
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
8190
that the authentication policy is holding (an expired password, or
8291
multi-factor sign-in required but not yet set up) is refused with `403`
8392
until it is fixed.
@@ -88,9 +97,9 @@ the server reads as not connected until you connect it again.
8897

8998
## Connect [#connect]
9099

91-
1. **Open the page.** Sign in to your server and go to **Setup → Apps →
92-
Cloud Connection**. The header reads *Cloud Connection — Connect this
93-
runtime to an ObjectStack control plane to browse your organization's
100+
1. **Open the page.** Sign in to your server and go to **Setup →
101+
Integrations → Cloud Connection**. The header reads *Cloud Connection —
102+
Connect this runtime to an ObjectStack control plane to browse your organization's
94103
private packages and install them here.* The panel shows *Checking
95104
connection…*, then **Not connected** and a **Connect** button.
96105
2. **Click Connect.** The server asks the cloud for a device code and hands
@@ -214,15 +223,16 @@ The routes the Cloud Connection page uses, all on the server's own origin:
214223
| `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. |
215224
| `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`. |
216225
| `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. |
217-
| `POST /api/v1/cloud-connection/unbind` | required | Disconnects; answers `{ "revoked", "cleared" }`. |
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. |
218227

219228
The settings that shape the flow are listed in
220229
[Environment variables](/docs/deployment/environment-variables#multi-tenant--cloud-control-plane):
221230
`OS_CLOUD_URL` picks the cloud, `OS_CLI_CLIENT_ID` overrides the OAuth client
222231
id the server presents (default `objectstack-cli`), and `OS_RUNTIME_VERSION`
223232
sets the version it reports. `OS_CLOUD_API_KEY` and `OS_ENVIRONMENT_ID` belong
224-
to cloud-hosted runtimes; a self-hosted server leaves both unset, and a set
225-
`OS_CLOUD_API_KEY` is used instead of the stored credential.
233+
to cloud-hosted runtimes; a self-hosted server leaves both unset. A set
234+
`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.
226236

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

‎content/docs/ui/setup-app.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ anchors are:
4848
| Group (anchor id) | Filled by |
4949
|:---|:---|
5050
| **Overview** (`group_overview`) | System Overview dashboard — `platform-objects` |
51-
| **Apps** (`group_apps`) | Packages — `platform-objects`; marketplace and [Cloud Connection](/docs/deployment/cloud-connection) — `cloud-connection` (capability plugin) |
51+
| **Apps** (`group_apps`) | Packages — `platform-objects`; marketplace — `cloud-connection` (capability plugin) |
5252
| **People & Organization** (`group_people_org`) | Users · Organization · Business Units · Teams · Organizations · Invitations — `platform-objects` |
5353
| **Access Control** (`group_access_control`) | Positions / Permission Sets — `plugin-security`; Sharing Rules / Record Shares — `plugin-sharing`; API Keys — `platform-objects` |
5454
| **Approvals** (`group_approvals`) | Approvals Inbox (the `approvals:inbox` component) · Requests · Action History · Delegations (OOO) — `plugin-approvals` |
5555
| **Configuration** (`group_configuration`) | All Settings · Localization · Company · Authentication · Email · File Storage · AI & Embedder · Knowledge · Feature Flags — `platform-objects`; Branding — `service-settings`, only where the `branding` settings namespace is offered (the `single` tenancy posture) |
5656
| **Diagnostics** (`group_diagnostics`) | Sessions · Notification Events — `platform-objects`; Audit Log — `plugin-audit` |
57-
| **Integrations** (`group_integrations`) | `plugin-webhooks` |
57+
| **Integrations** (`group_integrations`) | `plugin-webhooks`; [Cloud Connection](/docs/deployment/cloud-connection) — `cloud-connection`, except on a runtime holding the environment service key, which registers no entry |
5858
| **Advanced** (`group_advanced`) | OAuth Applications — `platform-objects` |
5959

6060
The exact rendered menu depends on which capability plugins are loaded.

‎docs/qa/platform-checklist/areas/platform-core.json‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2045,7 +2045,7 @@
20452045
"title": "Marketplace console honesty: nav entries live and die with their capability, the disabled state renders the configuration conclusion (never a red 'failed to load') for every viewer ahead of the admin guard, Installed Apps drives the real same-origin lifecycle, and an enabled runtime turns a non-admin away with access-denied",
20462046
"since": "v17",
20472047
"status": "active",
2048-
"revision": 5,
2048+
"revision": 6,
20492049
"priority": "P2",
20502050
"surface": "browser",
20512051
"personas": [
@@ -2067,15 +2067,15 @@
20672067
"boot B (OS_CLOUD_URL=off), as admin in Setup: confirm the group_apps nav carries Installed Apps (nav_marketplace_installed — the entry rides MARKETPLACE_INSTALLED_UI_BUNDLE, registered by the install-local plugin that IS mounted) and does NOT carry Browse Marketplace (nav_marketplace_browse rides the browse proxy, which is not mounted) — the marketplace-ui.ts ownership rule: 'the entry lives and dies with the capability → no dead page'",
20682068
"boot B: navigate directly to /apps/setup/system/marketplace — the page renders the MarketplaceDisabled card (data-testid marketplace-disabled): muted informational styling, a hint addressed to the operator naming the action, a Back-home button. Screenshot",
20692069
"boot B: open the Installed Apps page — the installed CRM entry renders with its manifest identity; open that package's Details page, where reseed and purge live in the 'More install options' menu (objectui#11627 — they are not on the Installed Apps list itself), drive both and capture the same-origin POSTs to /api/v1/marketplace/install-local/:manifestId/{reseed,purge}-sample-data on the wire",
2070-
"boot A (default), as admin: the group_apps nav now carries BOTH Browse Marketplace and Installed Apps plus Cloud Connection (nav_cloud_connection); open the browse page — if the control plane is reachable the catalog renders; if not, record blocked(environment) for the catalog leg only (the page mounting and asking is this kernel's part)",
2070+
"boot A (default), as admin: the group_apps nav now carries BOTH Browse Marketplace and Installed Apps, and the group_integrations nav carries Cloud Connection (nav_cloud_connection — it moved out of group_apps; a boot holding OS_CLOUD_API_KEY is platform-managed and carries no such entry at all); open the browse page — if the control plane is reachable the catalog renders; if not, record blocked(environment) for the catalog leg only (the page mounting and asking is this kernel's part)",
20712071
"as the plain member on boot A (marketplace enabled): navigate directly to /apps/setup/system/marketplace — once the admin verdict resolves (MarketplaceResolving holds only until then) the page renders MarketplaceAccessDenied, never an indefinite spinner and never the destructive card; then open a package URL (…/system/marketplace/:packageId) and confirm the package page issues no package request for the member. On boot B the same URLs render the MarketplaceDisabled card for the member too: objectui#5557 moved the disabled return AHEAD of the !isAdmin guard, because 'there is no marketplace here' is true of every viewer",
20722072
"on boot B reload the disabled page several times: confirm no transient red 'Failed to load' card paints before the disabled card settles (the objectui#5533/#5557 resolving-state work — MarketplaceResolving covers the gap)"
20732073
],
20742074
"acceptance": [
20752075
{
2076-
"clause": "nav ownership: on the offline boot, Installed Apps is present and Browse Marketplace is ABSENT from Setup's group_apps; on the cloud boot both are present (plus Cloud Connection) — each entry registered by the plugin that owns its capability, so no nav entry ever points at a dead page",
2076+
"clause": "nav ownership: on the offline boot, Installed Apps is present and Browse Marketplace is ABSENT from Setup's group_apps; on the cloud boot both are present (plus Cloud Connection, in group_integrations) — each entry registered by the plugin that owns its capability, so no nav entry ever points at a dead page",
20772077
"oracle": "api",
2078-
"verify": "GET /api/v1/meta/app?id=setup on each boot and diff the group_apps items (nav_marketplace_browse / nav_marketplace_installed / nav_cloud_connection presence per boot); confirm the rendered sidebar matches",
2078+
"verify": "GET /api/v1/meta/app?id=setup on each boot and diff the group_apps items (nav_marketplace_browse / nav_marketplace_installed presence per boot) and the group_integrations items (nav_cloud_connection presence per boot); confirm the rendered sidebar matches",
20792079
"evidence": "the two nav payloads + sidebar screenshots"
20802080
},
20812081
{
@@ -2147,6 +2147,12 @@
21472147
"date": "2026-10-16",
21482148
"change": "steps[2] and acceptance[2] (clause, verify) re-pointed (checklist-accuracy finding of QA run #23939, wave #23878). Reseed and purge are not on the Installed Apps list: they are in the package Details page's 'More install options' menu (objectui#11627), which is where #23939 drove them. The Installed Apps note #23939 saw as stale was objectui#12684; the console pin since moved that note into the Uninstall tooltip (objectui#12610). Not loosened: both POSTs must still leave the page same-origin and answer 2xx",
21492149
"ref": "#24036"
2150+
},
2151+
{
2152+
"revision": 6,
2153+
"date": "2026-10-21",
2154+
"change": "steps[3], acceptance[0].clause and acceptance[0].verify re-pointed: Cloud Connection (nav_cloud_connection) moved from group_apps to group_integrations, and a runtime holding the environment service key (OS_CLOUD_API_KEY) is platform-managed and registers neither the entry nor its page (ruling 甲 on #25097). The marketplace entries' ownership rule this item judges is unchanged.",
2155+
"ref": "#25097"
21502156
}
21512157
]
21522158
},

0 commit comments

Comments
 (0)