Skip to content

fix(cloud-connection): the install-local listing requires an authenticated principal, and narrows installedBy / storageDir to manage_metadata holders - #9256

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-9011-install-local-auth-floor
Aug 17, 2026
Merged

fix(cloud-connection): the install-local listing requires an authenticated principal, and narrows installedBy / storageDir to manage_metadata holders#9256
os-project-manager merged 2 commits into
mainfrom
claude/issue-9011-install-local-auth-floor

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes#9011

Implements the maintainer ruling of 2026-08-16 13:03Z (comment 5307568223): Option 3 — authenticated floor plus field narrowing on GET /api/v1/marketplace/install-local.

Premise re-measured on current main, not assumed

Branched from 23abe2782, i.e. the post-#8976 handler (PR #9012, e0695b582). Both of the card's claims hold:

1. The GET really is reachable with no credential — it is not covered by an upstream middleware.handleList's first statement was const { entries, skipped } = this.readAll();. The route is mounted on the raw framework app via httpServer.getRawApp(), and the only global middleware the Hono server installs are the Server-Timing collector, CORS, and the IHttpServer middleware seam — none of them authenticates. So the absence of a gate in the handler is the whole story: not a weaker gate, no gate.

2. Both narrowed fields were on the wire, and here is the complete field set. The ruling narrows two, so the rest are deliberately kept and are worth naming:

fieldscopedisposition
packageIdper itemkept for every authenticated caller
versionIdper itemkept
manifestIdper itemkept
versionper itemkept
installedAtper itemkept
withSampleDataper itemkept
installedByper itemnarrowed to manage_metadata holders
itemsper responsekept
totalper responsekept
storageDirper responsenarrowed to manage_metadata holders

The fork clause: no unauthenticated consumer exists

Checked every caller in both repos before writing code, and none fetches this route anonymously:

So Option 3 breaks no shipped behaviour, and the fork clause is not triggered. Two consequences worth recording: the console renders the "installed by" line conditionally (entry.installedBy && ...), so a narrowed caller sees the same list minus that one line rather than a broken render; and no console code reads storageDir at all.

The change

callerstatusitems / totalinstalledBystorageDir
anonymous401 UNAUTHENTICATED
authenticated, no manage_metadata200servedomittedomitted
authenticated, manage_metadata200servedservedserved

Tests — the three pinned postures, plus the direction proved

New packages/cloud-connection/src/marketplace-install-local-list-posture.test.ts pins all three rows. It declares in its own docblock that it is the file which answers "is the listing gated?", because three neighbours have names that sound like they do and do not.

The non-operator row is pinned in both directions — the inventory is present and the two fields are absent. Asserting only the absences would keep passing if that caller were refused outright, which is the option the ruling explicitly rejected for withdrawing a shipped console page. Refusals assert the ADR-0112 envelope (codeandstatus), never a bare failure.

Reverse verification, direction predicted before running. With the fix committed, the gate was ablated (resolveInstallPrincipal removed, operator forced true) and the suite re-run: 4 failed, 160 passed, every failure inside the new file and none outside it —

  • anonymous envelope: expected 200 to be 401
  • bare x-user-id header: expected 200 to be 401
  • refused before the ledger is read: expected 200 to be 401
  • non-operator narrowing: received keys included installedBy

The operator control stayed green under the ablation, as intended: it is the row the change does not move. The file was then restored with git checkout HEAD -- ... (index and tree both), leaving git status --porcelain empty.

marketplace-install-local-capability-enumeration.test.ts is touched for comments only: its docblock said the GET's posture was "a separate question tracked separately", which this PR makes stale. It now names the ruling and the file that pins it, and the "mounts the read listing" assertion records why the answer to an unauthorized read must stay a refusal rather than an unmounted route. No assertion was weakened or added.

Verification

All commands below were run at the final commit of this branch, dbdb23969.

  • pnpm --filter '@objectstack/cloud-connection^...' build — dependency closure built first (green).
  • pnpm --filter @objectstack/cloud-connection test21 files, 164 tests passed.
  • Gates derived with node scripts/pm/dispatch-gates.mjs against the four actually-changed paths, all green: check:changeset-gate-self-tests, check:objectui-changeset, check-adr-0087-registration.mjs, check-changeset-no-major.mjs, check-empty-changeset.mjs, docs-audit/check-affected-docs.mjs, plus the convention-triggered set for a new test file: check:query-options-erasure, check:type-check-coverage, check:type-check-debt (the --re-measure ratchet, run against a fully built workspace), check:engine-double-contract, check:where-matcher.
  • Also run because the diff implicates them: check:nul-bytes, check:authz-resolver, check:route-envelope, check:error-code-casing, check:error-status-conformance, check:adr-anchors (this file carries an ADR-0120/ADR-0104 anchor; its invariant is about the install seam and is untouched).

A changeset is included and is explicit that anonymous callers now receive 401 — a breaking change for any anonymous consumer, and for any authenticated non-operator consumer reading the two narrowed fields.


Generated by Claude Code

…cated principal, and narrows two fields
`GET /api/v1/marketplace/install-local` resolved no identity at all. After
#8976 gated the four mutating doors it was the only anonymous door left on
the surface: `handleList` opened on `this.readAll()`, so any caller who could
reach the port got 200 and the whole ledger — including `installedBy` (a
platform user id, enumerated across every install) and `storageDir` (an
absolute filesystem path on the host).
Maintainer ruling 2026-08-16, Option 3 — authenticated floor plus field
narrowing:
- anonymous -> 401 UNAUTHENTICATED
- authenticated, no `manage_metadata` -> 200, inventory without
`installedBy` / `storageDir`
- authenticated, `manage_metadata` -> 200, full payload unchanged
Identity comes from `resolveInstallPrincipal` — the same resolver the four
mutating doors already use — rather than a second session read, and the 401
envelope is extracted into one `refuseUnauthenticated` seam so the read door
and the write doors cannot drift apart on it. The mount stays unconditional
(cloud#1287); the answer to an unauthorized read is a refusal, not a 404.
No new capability is minted.
Fixes#9011
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/deployment/cli.mdx(via MarketplaceInstallLocalPlugin (symbol))
What this run could not see

Coarse fallback — 3 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 origin/mainpackageMentionDocs.

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 origin/main → pass the list as args.docs.

@github-actionsgithub-actionsBot added size/m documentation Improvements or additions to documentation tests tooling labels Aug 17, 2026
@os-project-manager
os-project-manager marked this pull request as ready for review August 17, 2026 08:50
@os-project-manager
os-project-manager added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit 01074e5Aug 17, 2026
27 of 28 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-9011-install-local-auth-floor branch August 17, 2026 09:00
os-zhuang pushed a commit that referenced this pull request Aug 18, 2026
…#9325)
The `lint` job in lint.yml publishes one check run whose name is its `name:`,
and that name was `ESLint` while the job ran ~70 sequential `pnpm check:*`
steps. Any one of them failing published a red check called `ESLint` on a PR
whose diff may have nothing to do with linting, so the reader had to open the
log and scroll to the failing step before they could even decide whose problem
it was. That cost was paid three times on 2026-08-17 (#9258, PRs #9256/#9291),
and the compounding harm is that a check whose name does not describe what it
verifies trains readers to re-run on red -- which is how a real regression in
this gate family gets waved through (#5617 is that failure, from the other
direction).
Renames the job to `Lint & Repo Gates` and moves the pinned literal in the
`check-required-contexts.mjs` registry (#6865) in the same diff, so the two
repo-side halves cannot drift. The self-test's rename ablation now anchors on
the new name and mutates it back to `ESLint`, which makes it the regression
test for this very change: reverting lint.yml without the registry is red.
⚠️ This is only the repo-side half. A GitHub required status check is matched
by check-run name, so the entry in Settings -> Rulesets must be swapped from
`ESLint` to `Lint & Repo Gates` by a maintainer immediately after this merges.
Merging without that swap leaves every open PR and the merge queue wedged on a
permanently-pending `ESLint` that can never report again.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NYgmGheCzM6NrHZN436Cxf
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GET /api/v1/marketplace/install-local serves the installed-package ledger to anonymous callers — no authentication at all

2 participants

@os-project-manager@claude