Found while implementing #9901. Filed rather than folded in: the 2026-08-20 maintainer ruling on that card enumerates four routes, and this is the fifth. Deciding it inside that PR would have been an un-ruled route quietly acquiring a capability.
Disclosure posture — inherited
⛔ No reproduction here, in any PR body, or in any commit message.#9901 inherited this posture from #9686 / #9391 for this same area, and so does this card. What follows is the source location and the contract question.
What is now true
registerExternalDatasourceRoutes (packages/rest/src/external-datasource-routes.ts) mounts five routes. After #9901 lands, four of them require a capability above the shared anonymous floor and one does not:
| route | admission after #9901 |
|---|
GET /:name/external/tables | authenticated + manage_platform_settings |
POST /:name/external/tables/:remote/draft | authenticated + manage_platform_settings |
POST /:name/external/tables/:remote/import | authenticated + manage_metadata |
POST /:name/external/refresh-catalog | authenticated + manage_metadata |
POST /:name/external/validate | authenticated, and nothing more |
The ruling's own words enumerate the read twins (external/tables, external/tables/:remote/draft) and the write pair (external/tables/:remote/import, external/refresh-catalog). validate appears in the #9901 body only obliquely — as the third of "three routes the admin spelling has no twin for, two of which write" — and no part of the ruling assigns it a capability.
Why it was left rather than decided
The two properties that pinned the other four do not reach this one:
That leaves a genuine question rather than an omission, which is why it is a card.
The open question
Should POST /external/validate require a capability, and if so which?
manage_platform_settings, treating it as a read of the same federation surface the two read twins expose. Argues from family coherence: an unentitled caller currently cannot list a datasource's remote tables but can run a validation pass over its federated objects, and a validation report is not nothing — it is a statement about which declared objects still match the remote schema.- Leave it on the authentication floor. Argues that a validity report over objects the caller can already see through normal metadata reads discloses nothing new, and that narrowing published SDK surface (
datasources.external.validate on ObjectStackClient, plus the CLI's datasource validate) without a measured need is scope the startup posture does not buy. - Something narrower — e.g. the ADR-0106 D4 read set (
studio.access / setup.access), which is what the sibling package-routes.ts requires for its reads rather than manage_platform_settings.
This is published SDK surface, so options 1 and 3 are behaviour changes for existing credentialed callers in exactly the way #9901's read routes were — the same migration question, at a smaller scale.
Where it is currently pinned
packages/rest/src/external-datasource-routes-auth-guard.test.ts — the #9901 PR gives this route its own explicit 'authenticated' kind and a case asserting it is still served to a caller holding nothing, so the current answer is written down rather than incidental. Changing it means changing that case, deliberately.
Backlinks: #9901 (the ruling that named the other four) · #9686 (this family's authentication floor) · #9593 (where the admin twin's capability was measured).
Unassigned and unqueued — for the triage seat to level and route.
Generated by Claude Code
Found while implementing #9901. Filed rather than folded in: the 2026-08-20 maintainer ruling on that card enumerates four routes, and this is the fifth. Deciding it inside that PR would have been an un-ruled route quietly acquiring a capability.
Disclosure posture — inherited
⛔ No reproduction here, in any PR body, or in any commit message.#9901 inherited this posture from #9686 / #9391 for this same area, and so does this card. What follows is the source location and the contract question.
What is now true
registerExternalDatasourceRoutes(packages/rest/src/external-datasource-routes.ts) mounts five routes. After #9901 lands, four of them require a capability above the shared anonymous floor and one does not:GET /:name/external/tablesmanage_platform_settingsPOST /:name/external/tables/:remote/draftmanage_platform_settingsPOST /:name/external/tables/:remote/importmanage_metadataPOST /:name/external/refresh-catalogmanage_metadataPOST /:name/external/validateThe ruling's own words enumerate the read twins (
external/tables,external/tables/:remote/draft) and the write pair (external/tables/:remote/import,external/refresh-catalog).validateappears in the #9901 body only obliquely — as the third of "three routes the admin spelling has no twin for, two of which write" — and no part of the ruling assigns it a capability.Why it was left rather than decided
The two properties that pinned the other four do not reach this one:
validateAllhas no spelling on@objectstack/service-datasource's admin family, so the twin-equivalence invariant (The same three datasource-admin routes answer 400 with the wrong service's error code (#4225 follow-up, one field over) #4249 failure contract, finding: the twolistRemoteTablesroute twins diverge on?schema=— the admin spelling drops it #7955 request shape, finding: the external-datasource federation HTTP family mounts outside the enforceAuth seam with no guard of its own — read and write #9686 anonymous axis, security(rest): the external-datasource federation family still admits any authenticated caller, while its declared admin twin now requires a capability #9901 capability axis) has nothing to converge it with. There is no measured sibling to copy from.manage_metadatareasoning that carriedimport/refresh-catalogdoes not carry it either.That leaves a genuine question rather than an omission, which is why it is a card.
The open question
Should
POST /external/validaterequire a capability, and if so which?manage_platform_settings, treating it as a read of the same federation surface the two read twins expose. Argues from family coherence: an unentitled caller currently cannot list a datasource's remote tables but can run a validation pass over its federated objects, and a validation report is not nothing — it is a statement about which declared objects still match the remote schema.datasources.external.validateonObjectStackClient, plus the CLI'sdatasource validate) without a measured need is scope the startup posture does not buy.studio.access/setup.access), which is what the siblingpackage-routes.tsrequires for its reads rather thanmanage_platform_settings.This is published SDK surface, so options 1 and 3 are behaviour changes for existing credentialed callers in exactly the way #9901's read routes were — the same migration question, at a smaller scale.
Where it is currently pinned
packages/rest/src/external-datasource-routes-auth-guard.test.ts— the #9901 PR gives this route its own explicit'authenticated'kind and a case asserting it is still served to a caller holding nothing, so the current answer is written down rather than incidental. Changing it means changing that case, deliberately.Backlinks: #9901 (the ruling that named the other four) · #9686 (this family's authentication floor) · #9593 (where the admin twin's capability was measured).
Unassigned and unqueued — for the triage seat to level and route.
Generated by Claude Code