Repository navigation
Commit ae58b26
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
File tree
- .changeset
- content/docs
- deployment
- ui
- docs/qa/platform-checklist/areas
- packages/cloud-connection/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
9 | | - | |
10 | | - | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
59 | 66 | | |
60 | 67 | | |
61 | 68 | | |
| |||
77 | 84 | | |
78 | 85 | | |
79 | 86 | | |
80 | | - | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
81 | 90 | | |
82 | 91 | | |
83 | 92 | | |
| |||
88 | 97 | | |
89 | 98 | | |
90 | 99 | | |
91 | | - | |
92 | | - | |
93 | | - | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
94 | 103 | | |
95 | 104 | | |
96 | 105 | | |
| |||
214 | 223 | | |
215 | 224 | | |
216 | 225 | | |
217 | | - | |
| 226 | + | |
218 | 227 | | |
219 | 228 | | |
220 | 229 | | |
221 | 230 | | |
222 | 231 | | |
223 | 232 | | |
224 | | - | |
225 | | - | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
226 | 236 | | |
227 | 237 | | |
228 | 238 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
| 51 | + | |
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
| 57 | + | |
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2045 | 2045 | | |
2046 | 2046 | | |
2047 | 2047 | | |
2048 | | - | |
| 2048 | + | |
2049 | 2049 | | |
2050 | 2050 | | |
2051 | 2051 | | |
| |||
2067 | 2067 | | |
2068 | 2068 | | |
2069 | 2069 | | |
2070 | | - | |
| 2070 | + | |
2071 | 2071 | | |
2072 | 2072 | | |
2073 | 2073 | | |
2074 | 2074 | | |
2075 | 2075 | | |
2076 | | - | |
| 2076 | + | |
2077 | 2077 | | |
2078 | | - | |
| 2078 | + | |
2079 | 2079 | | |
2080 | 2080 | | |
2081 | 2081 | | |
| |||
2147 | 2147 | | |
2148 | 2148 | | |
2149 | 2149 | | |
| 2150 | + | |
| 2151 | + | |
| 2152 | + | |
| 2153 | + | |
| 2154 | + | |
| 2155 | + | |
2150 | 2156 | | |
2151 | 2157 | | |
2152 | 2158 | | |
| |||
0 commit comments