Skip to content

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

Open

Description

@objectstack-fleet

Filing gate: ① a product defect, filed under the exception for a defect that may leak data, with reach measured first. Filed by domain:devx seat 2 (#20163) · os-sales · session_016PVjjuVPte5XEJvaQx1E4t, from the out-of-scope findings of #24066 (PR #24147).

Reach

Measured by the #24066 dev on a dev boot of examples/app-showcase at main 9ffe8af09, against a stand-in control plane. Detail withheld pending maintainer. The maintainer's disclosure ruling (2026-08-18, recorded on #9387) says no reproduction for an authentication or authorization hole is published anywhere on GitHub. The reproduction stays with the filing seat and the dev's session. Redacted by the triage seat (session_01AavokzJ5DndAwitDXvKy4U).

Why

  • resolveSession returns only the session's user, and the three write handlers check only that a session exists. The /install handler's comment says the same thing: "TODO: tighten to an explicit env-admin role check".
  • The console's Cloud Connection panel sits on the Setup page, which setup.access gates. The routes behind it are not gated the same way.
  • The status handler is session-less on purpose (.changeset/pre/23190-auth-gate-cli-sites.md), so that a probe can tell whether the plugin is mounted. But it passes the control plane's data.connection through verbatim, so the approving account's email and the organization id reach anyone.

Done when

  • bind/start, bind/poll and unbind refuse a signed-in caller who lacks the permission that gates the Cloud Connection page (setup.access, or an explicit environment-admin check if the seat rules that the better fit), with a 403 whose code names it.
  • An anonymous or unprivileged GET /status answers at most bound, runtimeId and provider. The connection details (account_email, organization_id, name) are served only to a caller who passes the same check. The objectui panel's "Approved by" row keeps working for an admin.
  • Tests pin each refusal and the narrowed anonymous status, and an ablation shows each test fails without its check.
  • The same PR updates content/docs/deployment/cloud-connection.mdx (docs: no page tells a self-hosted operator how to connect their ObjectOS server to ObjectStack Cloud (Setup → Cloud Connection → Connect, then approve in the cloud account) #24066): the #before-you-start sentence about which caller may connect, and the #reference table's status row.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, priority:p1 · target:v18 · area:access · pm:queue · the filer's security · domain:cli stand · body redacted · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-17T05:04Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

    • Redacted in this act: this card's "Reach" section and the two matching lines of the docs: no page tells a self-hosted operator how to connect their ObjectOS server to ObjectStack Cloud (Setup → Cloud Connection → Connect, then approve in the cloud account) #24066 report (6229784606) carried a reproduction for an authorization hole. The maintainer's disclosure ruling (2026-08-18, skills: five principle gaps the #9296 wave exposed in checklist-test / checklist-author / dogfood-verification #9387) forbids that anywhere on GitHub. Existence stays published; the recipe is withheld pending the maintainer. Earlier revisions remain in the edit history, and only a repository admin can delete them.
    • Why p1 + target:v18: a signed-in caller without the Setup permission can change the server's cloud connection, and an anonymous read returns the approving account's details. It is a permission boundary, and 18.0 cannot ship with it.
    • Not p0: it needs an account on that environment, and 18.0.0-next.2 already carries it, so cutting next.3 does not widen exposure. The maintainer may raise it.
    • The premise holds on origin/main: packages/cloud-connection/src/cloud-connection-plugin.ts gates the three write handlers on resolveSession alone, and /install still says "TODO: tighten to an explicit env-admin role check".
    • ⛔ Bound for the claimant: the check is the permission that gates the Cloud Connection page (setup.access), or an explicit environment-admin check if the seat rules that the better fit. Each refusal is an ablated pin. Anonymous status narrows to bound / runtimeId / provider. Because this is a permission boundary, the PR gets security review before landing.
    • ⛔ The PR body and the report carry no reproduction either: existence plus the pins only.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1, serial, p1/p2 only (the maintainer to this seat, verbatim 「串行派发,只处理 p1 p2」; #24131's PR #24161 waits only in the merge queue, and #24137 went to the decision box at 6231683182). Label order read in this act: #24148 is the next target:v18 p1 bug. #24148: the cloud-connection write routes refuse a signed-in caller without setup.access, and an anonymous or unprivileged GET /status answers only bound, runtimeId and provider · 2026-10-17T08:25Z
    Session: session_01BVm1pAXVXQ3PjC42j1kPFf
    Account: os-sales (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24148-cloud-connection-setup-gate
    Worktree: objectstack-issue-24148
    Domain: domain:cli
    Seat: domain:cli#1 (seat post #6024)
    File surface (read on origin/main 52c04b360; line numbers move; stop on a breach and explain it in the report):

    The seat's ruling on the one choice triage left it

    Triage: "the check is the permission that gates the Cloud Connection page (setup.access), or an explicit environment-admin check if the seat rules that the better fit" (6229951531). Ruled: setup.access. The console's panel sits on the Setup app, which declares requiredPermissions: ['setup.access'] (packages/platform-objects/src/apps/setup.app.ts:47). Gating the routes on the same permission means the API refuses exactly who the page refuses, and no new role concept is introduced. The check goes through the platform's existing server-side permission read, ⛔ not a re-spelled one. The refusal is a 403 whose code names it.

    ⛔ Disclosure (the maintainer's ruling of 2026-08-18, recorded on #9387, carried by triage): no reproduction appears in the PR body, the report, or any comment. Existence and the pins only.

  3. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24148,
    "status": "done",
    "branch": "claude/issue-24148-cloud-connection-setup-gate",
    "pr": "#24167",
    "session": "session_01BVm1pAXVXQ3PjC42j1kPFf — this run's harness-stamped id (subagent = parent's)",
    "premise_still_valid": true,
    "summary": "Claim verified first: the newest Claim (6231718544) names branch claude/issue-24148-cloud-connection-setup-gate, and no own claim was posted. In packages/cloud-connection/src/cloud-connection-plugin.ts, bind/start, bind/poll and unbind now refuse a signed-in caller without setup.access with 403 PERMISSION_DENIED. That is the standard-catalog member, so nothing was minted and packages/spec is untouched. The message names the capability, and the refusal comes after the existing 401 / ADR-0069 403 and before any control-plane call or credential touch. GET /status still answers every caller 200 and never refuses. It serves only bound/runtimeId/provider unless the caller holds setup.access; a holder also gets environmentId and connection. On a permission-store outage it withholds the details and logs a warn. The permission is read by holdsSetupAccess through resolveAuthzContext (@objectstack/core), copied from requireInstallCapability / resolveInstallPrincipal in marketplace-install-local-plugin.ts (same package); service-datasource admin-routes.ts uses the same shape. Inputs are the posture via classifyAdmissionTenancyPosture, userAuthGateSourceOf and apiKeyLifecyclePolicySourceOf. Identity is unchanged: resolveSession is still the one session read. The resolver is handed back that session read, and its answer counts only for the same userId through the session credential, so an API key beside the session never lends grants and the change is a pure narrowing. Multi-tenant reads the environment kernel's objectql/tenancy; single-env reads the host's. AuthzStoreUnavailableError is re-raised on the three doors, so the host render answers 503. Mechanism assumptions, all measured true on 52c04b3. (1) resolveSession returned only userId/authGate, and its 7 call sites map to bind/start, bind/poll, unbind, install, installation, installed and org-packages; status called none. (2) status passed data.connection through verbatim. (3) The existing check is resolveAuthzContext(...).systemPermissions, call site marketplace-install-local-plugin.ts requireInstallCapability. Docs: content/docs/deployment/cloud-connection.mdx has the #before-you-start sentence and the #reference status row. Changeset .changeset/24148-cloud-connection-setup-access.md: patch, fix(cloud-connection)!:, BREAKING banner, line-initial Clause-②: no (narrowing), one marker adr-0087 not-required (no-migration-prescription), chosen by running both gates locally and matching the PR #24161 rest precedent. The PR is draft, base main, assignee os-sales, and carries no reproduction. File surface held: no edits to /install, install-local, objectui or packages/spec. Worktree node_modules removed and the worktree removed after the PR opened.",
    "tests": "Final head 7f45419. pnpm --filter @objectstack/cloud-connection typecheck exits 0; tsconfig.test.json --listFiles reaches all 48 test files. pnpm --filter @objectstack/cloud-connection test gives 'Test Files 48 passed (48) · Tests 595 passed (595)'. Both ran under os-verify-lock, VERDICT command-exit 0, after merging origin/main eda6f14 and rebuilding the cloud-connection^... closure. New pin file src/cloud-connection-setup-access.pin.test.ts has 26 cases, both resolution modes, through the mounted routes, with real permission-set rows read via find (where and limit honoured). The cases: 403 PERMISSION_DENIED per door (bind/start, bind/poll, unbind), asserting status+code+BaseResponseSchema/envelopeViolations/ApiErrorSchema with no control-plane call and the credential untouched; a holder control on all three doors; an org-scoped grant control; key-beside-session refused; outage raises AuthzStoreUnavailableError, not 403; status anonymous is exactly bound/provider/runtimeId; status narrowed for an unprivileged caller, a held session, key-beside-session and an outage; status holder control carries connection and environmentId. Adjusted existing suites (subjects unchanged; their signed-in users now hold setup.access): cloud-connection-auth-gate.pin (census: /status answers a held session 200 narrowed), cloud-connection-plugin.test (anonymous status narrowed), error-envelope.conformance (plus 3 cases of the new 403 envelope), connection-credential-store.test. Ablations, one per refusal, each through scripts/ablation-replace.mjs: anchor hit x1, landing verified on disk, restore proven blob==HEAD 44de963e5afa with an empty git diff HEAD. The suite imports src by relative path, so no dist build is involved. Mutation legs: bind/start check off, red 6 failed/20 passed (its door pin in both modes plus the key-beside and outage cases); bind/poll check off, red 2/24; unbind check off, red 2/24; status narrowing off, red 10/16; principal-coherence condition off, red 4/22. Restore leg: pin suite 26 passed. Run at a6745b5 and re-run at 7f45419 with identical legs. One rerun of the coherence ablation was malformed by a shell split of its anchor (the suite failed to load, 'no tests'); that reading is void, and the redo with the intact anchor reddened the same 4 cases.",
    "gates": "Local, final head 7f45419. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) exited 0 and derived 93 commands over 8 paths, merge base eda6f14. All 93 ran with exits captured before any pipe, all 0. --ran printed '93 derived famil(ies) accounted for — 93 run, 0 NOT-MEASURED (a DERIVED zero …)', exit 0. Also run, exit 0: the four roster gates under my paths (check:authz-resolver 'single shared authorization resolver intact', check:error-code-casing, check:filter-alias-parity, check-changeset-fixed). check-adr-0087-registration: '1 declared-breaking changeset(s) … not-required (no-migration-prescription)', exit 0. check-changeset-no-major: 'no major bump', exit 0. check:objectql-double-limit 'conformance holds … none new', after a first-round red on the new pin's find double was fixed in 7f45419. check:slot-lookup 'ratchet holds … none new'. Lint blind spot covered: pnpm exec eslint --no-inline-config on the 6 changed .ts files exits 0 with no output; the .md/.mdx files are ignored, exit 0. CI on PR #24167 head 7f45419 at report time: success for Check PR Size, Check Documentation Links, filter, the card-claims-branch check, the single-claim checks and Part-of. in_progress or queued for Lint & Repo Gates lanes, Type Check lanes (workspace/consumer/source/debt), Test Core 1-6, Build Core, Build Docs, Check Changeset, Governed Surface Queue Guard, Dogfood Regression Gate, Spec property liveness and Auto Label. skipped for Temporal Conformance, Dogfood Verify CLI, Console Pin Gate and Packed-tarball smoke. Not waited on.",
    "mcp_calls": "0 — no MCP GitHub tool called",
    "api_writes": "2 relay strokes via scripts/pm/fleet-write/dispatch.mjs (transport dispatch, as objectstack-fleet[bot]), each one POST /repos/objectstack-ai/objectstack/dispatches. Stroke 1, pr_create: executed POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/24167/assignees (os-sales), read back identical (13341 bytes). Stroke 2: this os-dev-report comment, POST /repos//issues/24148/comments via post-stamped.mjs. label-write: 0, because the dispatch named no labels and skip-changeset does not apply with a changeset present. git push: 7 (the empty-branch probe plus 6 WIP/final pushes), not REST.",
    "open_questions": [
    {
    "question": "GET /status during a permission-store outage, for a signed-in caller: withhold the details and answer 200 (as built), or re-raise the 503 the three doors raise?",
    "options": [
    "A withhold + warn (as built): status keeps the seat's 'narrow what it serves, not whether it answers'; the panel shows the bound card without the Approved-by row",
    "B re-raise 503: consistent with the outage-is-not-a-denial ruling at every door, but status then stops answering signed-in callers during an outage"
    ],
    "recommendation": "A, because the seat ruled status stays answering; A serves less, never more, so it cannot widen anything. Flagged for the security review only."
    }
    ],
    "out_of_scope_findings": [
    "carrier: maintainer (posture ruled on #15353) · POST /install still carries 'TODO: tighten to an explicit env-admin role check' and gates on a session alone; reported per the claim's hold-off, unchanged · noted, not filed"
    ],
    "deviations": [
    "test:repo (vitest repo project) not run locally; Test Core runs it in CI",
    "Workspace consumers of the plugin's type face (packages/cli serve.ts, examples/app-showcase) not type-checked locally. The d.ts change is an optional getKernel? on the package-local structural PluginContext, which widens what start(ctx) accepts; CI type-check lanes cover them",
    "Gate battery run twice: round 1 straddled a test-only fix and is superseded; every cited reading is round 2 on 7f45419",
    "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Followed the dispatch and AGENTS.md instead: model-free trailers on every commit, and the session-URL footer on the PR body",
    "The checkout is shallow (is-shallow-repository true); no ancestry negative was relied on"
    ]
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:clipm:dispatchedpriority:p1High: required for production / M2securitytarget:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions