Repository navigation
Commit c71d03a
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
File tree
- .changeset
- content/docs/deployment
- packages/cloud-connection/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
| 19 | + | |
20 | 20 | | |
| 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 | + | |
| 21 | + | |
| 22 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
62 | | - | |
63 | | - | |
64 | | - | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
65 | 66 | | |
66 | 67 | | |
67 | 68 | | |
| |||
78 | 79 | | |
79 | 80 | | |
80 | 81 | | |
81 | | - | |
82 | | - | |
83 | | - | |
84 | | - | |
85 | | - | |
86 | | - | |
87 | | - | |
88 | | - | |
89 | | - | |
90 | | - | |
91 | | - | |
92 | | - | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
93 | 104 | | |
94 | 105 | | |
95 | 106 | | |
| |||
135 | 146 | | |
136 | 147 | | |
137 | 148 | | |
| 149 | + | |
138 | 150 | | |
139 | 151 | | |
140 | 152 | | |
| |||
216 | 228 | | |
217 | 229 | | |
218 | 230 | | |
219 | | - | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
220 | 237 | | |
221 | 238 | | |
222 | 239 | | |
223 | | - | |
| 240 | + | |
224 | 241 | | |
225 | 242 | | |
226 | | - | |
| 243 | + | |
227 | 244 | | |
228 | 245 | | |
229 | 246 | | |
| |||
232 | 249 | | |
233 | 250 | | |
234 | 251 | | |
235 | | - | |
| 252 | + | |
| 253 | + | |
236 | 254 | | |
237 | 255 | | |
238 | 256 | | |
| |||
0 commit comments