Skip to content

fix(cloud-connection)!: a runtime holding the env service key is platform-managed: no Setup page, disconnect refused; the entry moves to Integrations - #25110

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25097-cloud-connection-managed
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25097-cloud-connection-managed

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

…form-managed: no Setup page, unbind refused; the entry moves to Integrations

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…elf-hosted shapes; docs, checklist and changeset follow the move

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…y bindings, which never reach it

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…e, as what Setup Access alone still admits; rewrap two edited docs lines

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cloud-connection, touching 4 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via RESOURCE_CONFLICT (literal, a string literal in PLATFORM_MANAGED_UNBIND_REFUSAL))
  • content/docs/api/error-handling-server.mdx (via RESOURCE_CONFLICT (literal, a string literal in PLATFORM_MANAGED_UNBIND_REFUSAL))
  • content/docs/deployment/cloud-connection.mdx (via RESOURCE_CONFLICT (literal, a string literal in PLATFORM_MANAGED_UNBIND_REFUSAL))
  • content/docs/deployment/install-modes.mdx (via RESOURCE_CONFLICT (literal, a string literal in PLATFORM_MANAGED_UNBIND_REFUSAL))
  • content/docs/permissions/permission-sets.mdx (via RESOURCE_CONFLICT (literal, a string literal in PLATFORM_MANAGED_UNBIND_REFUSAL))
  • content/docs/ui/setup-app.mdx (via group_apps (literal, a string literal in group), group_integrations (literal, a string literal in group))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-6.mdx (via RESOURCE_CONFLICT (literal, a string literal in PLATFORM_MANAGED_UNBIND_REFUSAL))
  • content/docs/releases/v17/17-7.mdx (via RESOURCE_CONFLICT (literal, a string literal in PLATFORM_MANAGED_UNBIND_REFUSAL))
  • content/docs/releases/v18/18-0.mdx (via RESOURCE_CONFLICT (literal, a string literal in PLATFORM_MANAGED_UNBIND_REFUSAL))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see

Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ba244f1b487ffdf036c1b239757656801144cffe → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3aeea260f65650ba3964fd03d4116beabc422d4c — the merge of head b5445185ddd2dad21abda0696bd295ee55a20166 into base ba244f1b487ffdf036c1b239757656801144cffe, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3aeea260f65650ba3964fd03d4116beabc422d4c && git checkout 3aeea260f65650ba3964fd03d4116beabc422d4c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ba244f1b487ffdf036c1b239757656801144cffe b5445185ddd2dad21abda0696bd295ee55a20166 && git checkout -B drift-repro ba244f1b487ffdf036c1b239757656801144cffe && git merge --no-ff b5445185ddd2dad21abda0696bd295ee55a20166

node scripts/docs-audit/affected-docs.mjs --json ba244f1b487ffdf036c1b239757656801144cffe

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ba244f1b487ffdf036c1b239757656801144cffe → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b5445185ddd2dad21abda0696bd295ee55a20166
Local-runs: none
Scope: packages/cloud-connection/src/cloud-connection-plugin.ts, packages/cloud-connection/src/cloud-connection-ui.ts, packages/cloud-connection/src/cloud-connection-platform-managed.pin.test.ts, packages/cloud-connection/src/cloud-connection-auth-gate.pin.test.ts, packages/cloud-connection/src/cloud-connection-setup-access.pin.test.ts, packages/cloud-connection/src/error-envelope.conformance.test.ts, packages/cloud-connection/src/marketplace-installed-ui-entry.pin.test.ts + packages/platform-objects/src/apps/setup.app.ts (SETUP_APP: group_apps :60–65 ungated, group_integrations :104–109 requiredPermissions: ['manage_platform_settings'], app-level setup.access :47), packages/rest/src/meta-item-read-gate.ts (filterNav :1014–1018 and :1103–1105), packages/plugins/plugin-security/src/security-plugin.ts (:183–196, the organization_admin grant set) and packages/spec/src/identity/eval-user.zod.ts (ORGANIZATION_ADMIN_GRANTS), packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts (:255–256), packages/services/service-datasource/src/datasource-admin-plugin.ts (:573–574), packages/mcp/src/connect-ui.ts (:14–42 and :103–104), packages/spec/src/api/errors.zod.ts (:95, :189) and packages/spec/src/api/error-code-ledger.zod.ts (:56–70), packages/cloud-connection/src/marketplace-install-local-plugin.ts (:52–54, :1493–1499), docs/adr/0112-error-code-vocabulary-and-ledger.md (D2, D3), scripts/check-changeset-no-major.mjs (:58–96, :828), scripts/check-adr-0087-registration.mjs (:131–154), AGENTS.md (Post-Task Checklist step 3), the 19 other pending .changeset/*.md carrying Clause-②: no (narrowing), and the four prose faces (.changeset/25097-cloud-connection-platform-managed.md, content/docs/deployment/cloud-connection.mdx, content/docs/ui/setup-app.mdx, docs/qa/platform-checklist/areas/platform-core.json) judged in ②.

① Derived judgments

Check-runs on the head. Read twice (the 11:55Z batch, and again at record time): 31 success, 6 skipped (opt-in and matrix-placeholder jobs), and 4 still in_progress at record time — Test Core (1/6), (2/6), (3/6), (4/6). Those four are NOT MEASURED; this record judges on the diff. Every completed gate is green: Check Changeset, Governed Surface Queue Guard, every Lint and Type Check job, Build Core, Build Docs, Test Core (5/6) and (6/6), Spec property liveness, the claim and single-writer guards. The Tier S landing waits on the four shards whatever this verdict says.

Net diff. Merge-base 5050442bb; origin/main has since advanced to ba244f1b and touches none of the 11 files, nor setup.app.ts, so the diff reads the same against current main. 11 files, +608 / −57. No exported declaration added, removed or re-typed: index.ts is untouched, PLATFORM_MANAGED_UNBIND_REFUSAL is a module-private const, and the plugin's three exports are unchanged.

  1. credential() — RIGHT, behaviour-identical on every runtime. Before: cloudApiKey || this.store.read()?.runtimeToken || ''. After: platformManaged ? cloudApiKey : this.store.read()?.runtimeToken || '' with platformManaged = cloudApiKey !== '' and cloudApiKey already trimmed at :304. Key non-empty: both arms return the key. Key empty: both arms return the stored token or ''. The store read stays inside the closure, per call, so a runtime without the key that completes a device bind while up presents the new oscc_ token on the next request exactly as before. A runtime with the key never presented the stored token before either; nothing moves there.

  2. Registration — RIGHT, ruling 甲 row 1. manifest.register(CLOUD_CONNECTION_UI_BUNDLE) now sits in the else of if (platformManaged) (:400–411); the managed arm logs one info line. The predicate is read once at kernel:ready, at the same point cloudApiKey has always been resolved, so a page cannot register for a key set later — matching the comment's claim. No page, no nav_cloud_connection, on a keyed runtime.

  3. The 409's position — RIGHT on every path. The unbind handler (:819–872) is one linear sequence with no branch before the refusal: 404 ENVIRONMENT_NOT_FOUND (multi-env without an id) → held-session 403 → 401 UNAUTHENTICATED → holdsSetupAccess 403 PERMISSION_DENIED (an AuthzStoreUnavailableError re-raises to the host's 503, the pre-existing outage path) → if (platformManaged) 409 → revoke fetch → store write or clear. Neither the revoke nor the store is reachable before the 409, and a caller without a session or without setup.access learns nothing about how the credential is held. The managed-shape CONTROL in the setup-access pin runs the 403 cases on the keyed mount, so the 403-before-409 ordering is pinned, not only read.

  4. Error code — RIGHT. RESOURCE_CONFLICT is the standard catalog's 409 member (errors.zod.ts :95, :189). The ledger's own rule ("Registering a new code", error-code-ledger.zod.ts :56–70, mechanical since [finding] @objectstack/rest registers four generic synonyms the standard catalog already covers (CONFLICT, NOT_FOUND, FORBIDDEN, INTERNAL) — contract call, not a cleanup #8211) sends a generic condition to the standard catalog rather than a registered synonym; a minted code would need a packages/spec ledger entry the dispatch held off. The precedent cited is real: the install-local door answers 409 RESOURCE_CONFLICT for a refusal that comes from runtime state (marketplace-install-local-plugin.ts :52–54, :1495). The envelope is pinned against BaseResponseSchema, envelopeViolations and ApiErrorSchema in the new pin and in error-envelope.conformance.test.ts.

  5. Status route — RIGHT, unchanged. No hunk touches :587–636. The served shape is still { bound, runtimeId, provider } for a caller without setup.access and the full record otherwise; bound is still Boolean(credential()), which item 1 shows unchanged. No managed field. The claim's hold-off is honoured.

  6. Nav move — RIGHT, ruling 甲 row 2. CLOUD_CONNECTION_NAV_CONTRIBUTIONS[0].group moves 'group_apps' → 'group_integrations', priority 200 kept; the item and its type are unchanged. The other contributors read where declared: webhooks 100, datasources 100, connect-agent 110, so 200 sorts last, as the ui.ts comment states. One exported constant's value changes (and CLOUD_CONNECTION_UI_BUNDLE carries it); its type does not.

  7. The nav move's permission consequence — RIGHT as read, implied by the placement, two rules remain, not a block. Measured in the cited code: SETUP_APP.group_integrations declares requiredPermissions: ['manage_platform_settings'] (setup.app.ts :108) while group_apps declares none (:60–65); filterNav (meta-item-read-gate.ts :1017) drops a group entry whose requiredPermissions the caller lacks, children included, server-side, so the entry is not SERVED to such a caller, not merely hidden; and organization_admin holds setup.access, setup.write, manage_org_users and deliberately not manage_platform_settings (security-plugin.ts :183–196). So on a runtime without the key an organization administrator who could open the entry from group_apps no longer receives it. Judgments: (a) it is a consequence the ruling's placement implies — the ruling named the group, the gate is that group's own declared, pre-existing property, it already applies to every sibling entry (webhooks, datasources, connect-agent), and packages/mcp/src/connect-ui.ts :25–34 records that the app-level setup.access gate and the group gate were measured together once before and kept — it is not a defect the diff introduced; (b) yes, it leaves two rules for one capability: the entry is served on manage_platform_settings, while the three doors (bind/start, bind/poll, unbind) and the page itself (cloud_connection_settings carries no requiredPermissions of its own and is reachable by URL) take setup.access alone; the narrowing runs in the safe direction (fewer see, nobody new may act, the doors are unchanged), it is declared no (narrowing), it is row 5 of the PR's Contract table and a sentence in the changeset and in cloud-connection.mdx; (c) it does not block — it belongs to the maintainer as a question (③ item 2). One nit, not a block: the plugin's cloud-connection: bind/start, bind/poll and unbind admit any signed-in user (no Setup Access check), and GET /status serves the approving account's email and organization id to anonymous callers #24148 header (:45–59, "the three doors ... refuse exactly who that page refuses") still holds for the page and no longer for the entry; cloud-connection-ui.ts :70–71 records the group gate, the plugin header does not.

  8. Existing pins — RIGHT, none loosened. cloud-connection-auth-gate.pin.test.ts: mountCloudConnection gains a serviceKey parameter defaulting to the key, so every HELD case still runs on the shape it always ran on; the UN-HELD control splits into self-hosted (all seven doors 200, unbind driven last because it really clears the credential) and managed (six doors 200, unbind 409 with no control-plane call). cloud-connection-setup-access.pin.test.ts: the CONTROL likewise reads both shapes; bind start and bind poll keep every prior assertion in both; the 403 refusals still run on the keyed mount. marketplace-installed-ui-entry.pin.test.ts: the group_apps control switches to nav_marketplace_browse with MARKETPLACE_BROWSE_UI_BUNDLE registered in the composition, and the shell gains the group_integrations anchor; the page-listing control is unchanged. error-envelope.conformance.test.ts: the 409 case is added beside the others. A '' key is '' ?? env — not nullish — so the env spelling is not consulted, as both fixtures' comments say.

  9. New pin — RIGHT. 8 cases. Registration is read back through a real ObjectKernel + ObjectQLPlugin metadata protocol with lit controls in the same composition (control_settings served, nav_marketplace_browse in group_apps), under both spellings of the key (controlPlaneApiKey: undefined falls through ?? to OS_CLOUD_API_KEY), plus the not-managed reading pinning the entry as the only child of group_integrations, absent from group_apps, once in the tree. The boot line is pinned to exactly one. Unbind is driven in both resolution modes: managed 409 in the declared envelope, message's first sentence pinned, zero control-plane calls, stored credential untouched; control 200 with exactly one revoke call and the credential cleared. The unfiltered protocol read pins registration, not the group gate; that gate is filterNav's and is pinned there, which is the right layer. No read escapes the package.

  10. Named pins left unmodified — reasons read right. platform-page-i18n-parity.test.ts reads the static bundle's pages, and the page is unchanged; check-app-nav-i18n.mjs keys labels by app and nav id (apps.setup.navigation.nav_cloud_connection), which the regroup does not touch; platform-app-object-entry-views.test.ts judges type: 'object' entries and this one is type: 'page'. The claim asked that they be updated, not loosened; an update with nothing to update is the honest reading. Their CI shards are among the four not yet measured.

  11. Docs — consistent with the behaviour. cloud-connection.mdx: the Setup path, a managed-server paragraph (no page, no entry, one boot line, 409 with nothing revoked or cleared), the group-gate sentence that records the two-rule state plainly, and the reference row for the disconnect request. setup-app.mdx: the Apps and Integrations rows. platform-core.json: platform-core.marketplace-console-honesty steps[3] and acceptance[0] re-pointed, revision 6 with a history entry; the item's ownership rule is unchanged.

  12. Bind/start and bind/poll on a managed runtime — unchanged, and correctly out of scope. Both doors still run and bind/poll still writes a device credential that credential() never presents while the key is set (the managed CONTROL in the setup-access pin shows oscc_new persisted, then the refused unbind leaving it in place). That shape is main's today (the old credential() preferred the key too) and the card holds the binding flow off. Escalated in ③, not a block.

② Semver level

The diff publishes from @objectstack/cloud-connection (18.0.0, not private). What changes is served behaviour: a keyed runtime refuses a disconnect it carried out and no longer registers the page or entry; a key-less runtime serves the entry in a gated group. Nothing widens — no new key on any published payload (the status shape is unchanged, the 409 rides the declared envelope, the boot line is a log), no new export, no removed export. So Clause-②: no (narrowing) is the right line, on the PR body and in the changeset, and the review face is the narrowing face at CONTRACT_REVIEW_TIER.

Level: minor is right; the card's patch is not. AGENTS.md Post-Task Checklist step 3: "(narrowing) is BREAKING". scripts/check-changeset-no-major.mjs (:58, :79, :828) records the launch-window convention under which a breaking change ships as minor, and refuses major. All 20 pending changesets carrying Clause-②: no (narrowing), this one included, grade their primary package minor. A patch would have declared a narrowing non-breaking against the rule file that wins over the card. The changeset carries the BREAKING banner and the adr-0087 marker not-required (no-migration-prescription) in the comment form the gate reads; the category is honest — the body carries no rewrite instruction (the "Nothing to rewrite" bullet is a statement, not a prescription), nothing authorable moves, and the only exported change is a value inside a constant of unchanged type. Check Changeset is success on the head. The changeset describes behaviour only, in keeping with the #9387 discipline the card names.

③ Boundary flags

The dev's report is comment 6315317491 on #25097: eight deviations, two out-of-scope findings, no open questions.

  1. Changeset level minor versus the card's patch — answered: minor, per ②.

  2. Nav-permission consequence (entry served on manage_platform_settings, doors on setup.access) — answered in ① item 7; escalated to the maintainer as a question, not a block. The question for the maintainer: ruling 甲 placed the entry in Integrations for prominence; its group gate means a setup.access-only administrator (the default organization_admin set) on a self-hosted runtime no longer sees the entry yet may still open the page by URL and drive connect and disconnect. Should the three doors and the page follow the group's manage_platform_settings so one rule governs the capability, or should the entry stay exactly where the ruling put it with the split recorded, as this PR does? Either answer is a new ruling; this PR's declared narrowing stands on its own.

  3. File surface beyond the claim (auth-gate and setup-access pins, the envelope conformance test, three docs, the changeset) — answered: each is a file the behaviour change falsified or the dispatch's convention pointer named; none drops a prior assertion. The request to amend the claim's file surface is the seat's act.

  4. Named pins needing no edit — answered: ① item 10.

  5. RESOURCE_CONFLICT with no minted code — answered: ① item 4.

  6. Attribution — not a contract matter. All four commits carry the model-free trailer pair AGENTS.md prescribes; no model identifier in the trailers, the PR body or the changeset.

  7. A stopped lint run of the dev's own — not a contract matter; the ESLint job on the head is success.

  8. skills: five principle gaps the #9296 wave exposed in checklist-test / checklist-author / dogfood-verification #9387 discipline — answered: the PR body's Contract table describes answers and effects, no request shapes.

Out-of-scope finding 1 — bind/start and bind/poll on a managed runtime — escalated as a follow-up card for the lane seat, not a block. It is the symmetric half of what this card fixed: a hand-driven bind on a keyed runtime presents the service key to the control plane's /bind and persists a local credential the key shadows, so the cloud-side row and the local store can be moved by a door the runtime cannot act on. The dev's "zero pull" reading holds only while nothing in the product reaches those doors, and they are public routes. The ruling's table carries one Unbind column and the card names the binding flow out of scope, so this PR is right not to touch them; Prime Directive #10 makes a reproducible defect card material rather than an acceptance note, so the carrier should be a card, not "none".

Out-of-scope finding 2 — setup-app.mdx's Integrations row omitting Datasources and Connect an Agent — answered: pre-existing drift, correctly not widened here; a docs-only PR is the carrier.

Not measured — Test Core (1/6) through (4/6) were in_progress at both reads. The verdict below is on the diff; the owning seat lands only once every check is green.

Implemented-by: claude/issue-25097-cloud-connection-managed
Reviewed-by: session_01LmjU2x1faLZ39DVTFyVca6

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT

domain:cli seat 2 (#22648) · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-21T12:13Z · head b5445185ddd2dad21abda0696bd295ee55a20166 · claim 6313831494 → report 6315317491 → review of record 6315584966 (CONTRACT_REVIEW_TIER, PASS).

Check Reading
Shape Draft, base main. First line Fixes #25097; Clause-②: no (narrowing) line-initial. Assignee os-justin = the card's. One changeset, @objectstack/cloud-connection: minor, with the BREAKING banner and adr-0087: not-required (no-migration-prescription). The commits carry AGENTS.md's model-free trailer pair.
Surface 11 files, +608 / −57. check-governed-merges.mjs --pr 25110 on main ba244f1b48: NOT governed. git merge-tree against that main: clean.
Mechanism Read in the diff. One predicate, platformManaged, and credential() is written in terms of it. The review shows the behaviour is identical for every runtime without the key, including a device bind completed while up. A managed runtime registers no page or entry and logs that once. unbind answers 409 RESOURCE_CONFLICT after the identity and setup.access checks and before the revoke and the store write, on every path. The status shape is unchanged, with no managed field. The nav entry moves to group_integrations at priority 200, last.
Evidence The new pin (8 cases) is 6 red / 2 green on main and 8 green after. Restoring both source blobs hash-equal reds 10 assertions across 5 suites, every one new, and the change was restored. @objectstack/cloud-connection: 54 files / 637 tests. The three named pins are green unmodified, for the reasons the review confirms (① 10).
Gates Local: 97/97 derived families and the 10 Declared WIDE population families exit 0; pnpm lint exit 0. Check-runs on this head, read at this act: 34 success, 6 skipped, 1 in_progress (Test Core 3/6). Landing waits on it.

Accepted deviations:

  • Changeset minor, not the card's patch: right per AGENTS.md ("(narrowing) is BREAKING") and the launch-window convention (review ②).
  • File surface beyond the claim: cloud-connection-auth-gate.pin.test.ts, cloud-connection-setup-access.pin.test.ts, error-envelope.conformance.test.ts, content/docs/deployment/cloud-connection.mdx, content/docs/ui/setup-app.mdx and docs/qa/platform-checklist/areas/platform-core.json. Each is a file the behaviour change falsified or the dispatch's convention pointer named, and none drops a prior assertion (review ③ 3). This ACCEPT amends the claim's file surface to the PR's 11 files.
  • The nav entry is now served on manage_platform_settings (the Integrations group's own gate), while the doors and the page keep setup.access. The ruling's placement implies it, it runs in the safe direction, and it is recorded in the Contract table, the changeset and the docs (review ① 7). Whether one rule should govern both goes to the maintainer as a new question. It does not hold this landing.

Out-of-scope dispositions (one line each):

Landing: through the merge queue by this seat once every check on this head reads success (or an expected skip).


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 21, 2026 12:19
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 21, 2026 12:19
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit ae58b26 Oct 21, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25097-cloud-connection-managed branch October 21, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants