Found while implementing #8976 (the capability gate on the four mutating install-local routes). Out of scope there by ruling — that card's 裁决 names the four mutating doors and says the fix is authorization on those — so filing separately rather than riding it in. Unassigned.
Measured (origin/main, packages/cloud-connection/src/marketplace-install-local-plugin.ts)
handleList — the handler mounted at GET /api/v1/marketplace/install-local — calls no identity resolution whatsoever. Its first statement reads the ledger:
privatehandleList=async(c: any,ctx: PluginContext): Promise<Response>=>{const{ entries, skipped }=this.readAll();The four sibling routes (POST /install-local, DELETE …/install-local/:id, …/reseed-sample-data, …/purge-sample-data) each opened with requireAuthenticatedUser before #8976 and now open with a manage_metadata capability gate. The GET opens with nothing — it is not a weaker gate, it is the absence of one, so an anonymous caller who can reach the port gets a 200.
Re-check by symbol (line numbers in this repo go stale within hours):
git grep -n "handleList" -- packages/cloud-connection/src/marketplace-install-local-plugin.ts
git grep -n "rawApp.get(ROUTE_BASE" -- packages/cloud-connection/src/marketplace-install-local-plugin.ts
What the response body discloses
Per ledger entry: packageId, versionId, manifestId, version, installedAt, installedBy, withSampleData — plus, once per response, storageDir.
installedBy is a platform user id, and the listing enumerates them across every install.storageDir is an absolute filesystem path on the host (#6721 added it deliberately, for a signed-in CLI operator who cannot see the remote host's disk).- The package inventory itself is a version-level software bill of materials for the deployment — which packages, at which versions, installed when.
Why it is worth a decision rather than an obvious patch
The mount is deliberately unconditional (cloud#1287, so air-gapped boxes stop 404ing), and the console's "Installed Apps" page is a real consumer of this route, so the question is which posture the read should take, not whether to remove it. At least three defensible answers:
- Authenticated floor — any signed-in principal may list. Cheapest, matches "the console shows it in Setup".
- The same
manage_metadata the write doors now demand — consistent with the operator-grade framing of the surface, but would hide the Installed Apps list from non-operator users the console currently shows it to. - Authenticated floor + field narrowing — list the packages, drop
installedBy and storageDir for callers without an authoring capability. Splits "what is installed here" from "who installed it and where it lives on disk", which are genuinely different sensitivities.
The storageDir half interacts with #6721's stated purpose, so whichever way it goes should be decided knowingly rather than as a side effect.
Severity
Read-only disclosure, so lower than #8976's write channel — but it needs no credential of any kind, and on the walled multi-org EE shape (cloud#1331's surveyed deployment) the inventory and the installer identities are cross-tenant information the Layer 0 wall does not scope either, for the same reason: metadata is environment-scoped, not org-scoped.
Related: #8976 (the mutating routes; now gated), #6603 / #8919 (the /meta write-door precedent), cloud#1331 (walled-deployment context), #6721 (why storageDir is in this payload).
Found while implementing #8976 (the capability gate on the four mutating install-local routes). Out of scope there by ruling — that card's 裁决 names the four mutating doors and says the fix is authorization on those — so filing separately rather than riding it in. Unassigned.
Measured (
origin/main,packages/cloud-connection/src/marketplace-install-local-plugin.ts)handleList— the handler mounted atGET /api/v1/marketplace/install-local— calls no identity resolution whatsoever. Its first statement reads the ledger:The four sibling routes (
POST /install-local,DELETE …/install-local/:id,…/reseed-sample-data,…/purge-sample-data) each opened withrequireAuthenticatedUserbefore #8976 and now open with amanage_metadatacapability gate. TheGETopens with nothing — it is not a weaker gate, it is the absence of one, so an anonymous caller who can reach the port gets a 200.Re-check by symbol (line numbers in this repo go stale within hours):
What the response body discloses
Per ledger entry:
packageId,versionId,manifestId,version,installedAt,installedBy,withSampleData— plus, once per response,storageDir.installedByis a platform user id, and the listing enumerates them across every install.storageDiris an absolute filesystem path on the host (#6721added it deliberately, for a signed-in CLI operator who cannot see the remote host's disk).Why it is worth a decision rather than an obvious patch
The mount is deliberately unconditional (cloud#1287, so air-gapped boxes stop 404ing), and the console's "Installed Apps" page is a real consumer of this route, so the question is which posture the read should take, not whether to remove it. At least three defensible answers:
manage_metadatathe write doors now demand — consistent with the operator-grade framing of the surface, but would hide the Installed Apps list from non-operator users the console currently shows it to.installedByandstorageDirfor callers without an authoring capability. Splits "what is installed here" from "who installed it and where it lives on disk", which are genuinely different sensitivities.The
storageDirhalf interacts with #6721's stated purpose, so whichever way it goes should be decided knowingly rather than as a side effect.Severity
Read-only disclosure, so lower than #8976's write channel — but it needs no credential of any kind, and on the walled multi-org EE shape (cloud#1331's surveyed deployment) the inventory and the installer identities are cross-tenant information the Layer 0 wall does not scope either, for the same reason: metadata is environment-scoped, not org-scoped.
Related: #8976 (the mutating routes; now gated), #6603 / #8919 (the
/metawrite-door precedent), cloud#1331 (walled-deployment context), #6721 (whystorageDiris in this payload).