From 2cd3989f728336b226a4fa6a41a50a4f24981384 Mon Sep 17 00:00:00 2001 From: Alban Mouton Date: Mon, 1 Jun 2026 14:37:24 +0200 Subject: [PATCH 1/6] docs: design spec for plugin-info permission transitivity Co-Authored-By: Claude Opus 4.8 (1M context) --- ...gin-info-permission-transitivity-design.md | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-01-plugin-info-permission-transitivity-design.md diff --git a/docs/superpowers/specs/2026-06-01-plugin-info-permission-transitivity-design.md b/docs/superpowers/specs/2026-06-01-plugin-info-permission-transitivity-design.md new file mode 100644 index 0000000..2666d73 --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-plugin-info-permission-transitivity-design.md @@ -0,0 +1,106 @@ +# Plugin info through the processing's permission boundary + +**Date:** 2026-06-01 +**Branch:** `fix-permission-transitivity` +**Status:** Approved design + +## Problem + +A user can be granted an *individual* permission (`read` / `exec` / `admin` via +the processing's `permissions` array) on a processing they do not own. Such a +user can open the processing, but the UI fetches the associated plugin's +metadata by calling the registry **directly from the browser**, authenticated +as that user: + +``` +GET /registry/api/v1/artefacts/:pluginId (ui/src/pages/processings/[id]/index.vue:157) +GET /registry/api/v1/artefacts/:pluginId (ui/src/composables/use-plugin-fetch.ts, used by processing-card.vue) +``` + +When the plugin is private and the **user** has no personal grant on it, the +registry returns 403/404. The UI interprets that as `pluginBroken`, shows the +"plugin broken" banner and suppresses the config form — even though the user is +legitimately allowed to see the processing. + +The registry access *should* be evaluated against the processing's **owner** +(who, by construction, has a working grant — the processing runs as them), not +against the individual viewer. The existing `GET /:id/plugin-config-schema` +endpoint already does exactly this: it gates on the processing permission of the +**user**, then calls the registry on behalf of the **owner** +(`ensurePluginAndReadSchema` → `ensureArtefact({ account: processing.owner })`). +Only the metadata fetch was left going direct. + +## Fix + +Route plugin **metadata** through the processing API under the same +"processing-permission-on-the-user, then registry-grant-on-the-owner" rule, and +repoint both UI surfaces at it. + +### API — new endpoint + +`GET /api/v1/processings/:id/plugin` in `api/src/processings/router.ts`: + +1. `session.reqAuthenticated(req)`, load the processing (404 if missing). +2. Gate identically to `/plugin-config-schema`: + `getUserResourceProfile(owner, permissions, sessionState)` ∈ `{admin, exec, read}` + → else 403. **Permission evaluated against the user.** +3. Fetch artefact metadata from `config.privateRegistryUrl`: + `GET /api/v1/artefacts/:pluginId` with headers + `x-secret-key: config.secretKeys.registry` and + `x-account: JSON.stringify(owner)`. **Registry grant evaluated against the + owner.** A lightweight authenticated GET — *not* `ensureArtefact`, which + downloads and extracts the whole tarball (too heavy for metadata). +4. Return the artefact JSON. Translate registry errors at the boundary the same + way `ensurePluginAndReadSchema` does: 403 → 403, 404 → 404 (so the UI's + `pluginBroken` still fires for genuinely revoked/deleted plugins), anything + else → 502. + +Refactor: extract the shared registry-header construction +(`{ 'x-secret-key', 'x-account': JSON.stringify(account) }`) and the +status-translation so the new endpoint and `ensurePluginAndReadSchema` don't +drift apart. + +### UI — repoint both surfaces + +- `ui/src/pages/processings/[id]/index.vue` (~L155): change the `useFetch` URL + from `/registry/api/v1/artefacts/:id` to + `${$apiPath}/processings/${processingId}/plugin`. +- `ui/src/composables/use-plugin-fetch.ts`: change `usePluginFetch` to take the + **processing id** (cache keyed by it) and fetch + `${apiPath}/processings/:id/plugin`. The returned `RegistryArtefact` shape is + unchanged, so consumers need no further change. +- `ui/src/components/processing/processing-card.vue` (~L194): pass the + processing id to the widened `usePluginFetch`. + +The plugin-**picker** flows (`ui/src/pages/processings/new.vue`, +`ui/src/components/processings-actions.vue`) keep their direct +`/registry/api/v1/artefacts` list calls — those are owner/admin creation +contexts where the user already has registry access. Out of scope. + +## Tests + +API test under `tests/features/processings/` (alongside +`plugin-access.api.spec.ts`): + +- **Regression target:** a user with an individual `read` permission on a + processing whose **owner** has plugin access — but who has **no** personal + grant — gets `200` from `GET /:id/plugin`. +- No permission on the processing → 403. +- Owner itself lacks the plugin grant → 403. +- Unknown / deleted plugin → 404. + +## Docs + +- Update `docs/architecture/v6-registry-integration.md`: the section describing + the UI fetching plugin metadata same-origin from `/registry` is now partly + superseded — plugin **metadata** flows through the processing API under the + processing-permission-then-owner-grant rule. Note this explicitly. + +## Out of scope / known follow-up + +- **Thumbnails.** Plugin thumbnails are only rendered in the picker + (`new.vue:75`, owner/admin context). The list cards and detail page carry the + `thumbnail` field in the type but do not display it, so there is nothing to + forward today. If a thumbnail is ever shown on a permitted-but-ungranted + user's card/detail, it would 403 the same way and need an equivalent + processing-scoped proxy. Noted, not built. From db60780576075756d2a18258ecf09c5f8128782c Mon Sep 17 00:00:00 2001 From: Alban Mouton Date: Mon, 1 Jun 2026 14:51:58 +0200 Subject: [PATCH 2/6] feat(api): plugin metadata endpoint scoped by processing permission Co-Authored-By: Claude Opus 4.8 (1M context) --- api/src/processings/router.ts | 81 +++++++++++++++---- .../plugin-info-transitivity.api.spec.ts | 81 +++++++++++++++++++ 2 files changed, 145 insertions(+), 17 deletions(-) create mode 100644 tests/features/processings/plugin-info-transitivity.api.spec.ts diff --git a/api/src/processings/router.ts b/api/src/processings/router.ts index da6eeff..b50cff9 100644 --- a/api/src/processings/router.ts +++ b/api/src/processings/router.ts @@ -14,6 +14,7 @@ import eventsQueue from '@data-fair/lib-node/events-queue.js' import { reqOrigin, session } from '@data-fair/lib-express/index.js' import { ensureArtefact } from '@data-fair/lib-node-registry' import { httpError } from '@data-fair/lib-utils/http-errors.js' +import { axiosBuilder } from '@data-fair/lib-node/axios.js' import { createNext } from '@data-fair/processings-shared/runs.ts' import { importPluginModule } from '@data-fair/processings-shared/plugin-load.ts' import { applyProcessing, deleteProcessing } from '../runs/service.ts' @@ -46,18 +47,36 @@ const ajv = ajvFormats(new Ajv({ strict: false, addUsedSchema: false })) * the schema extracted into a standalone file. Returns `undefined` when * neither source carries one. */ +/** + * The registry evaluates artefact access against an `x-account` header. For a + * processing that is always the OWNER — anyone allowed to see the processing + * inherits the owner's plugin access transitively. + */ +const registryAccount = (owner: Processing['owner']) => ({ + type: owner.type, + id: owner.id, + ...(owner.department ? { department: owner.department } : {}) +}) + +/** + * `lib-express/error-handler.js` collapses every AxiosRequestError to 500, so + * we translate registry failures into user-facing French messages at the + * boundary. 403/404 keep their status (the UI relies on them to show its + * "plugin broken" banner); anything else is a registry outage → 502. + */ +const translateRegistryError = (err: any, processing: Pick) => { + const status = err?.status ?? err?.statusCode + const ownerLabel = processing.owner.name ?? processing.owner.id + if (status === 403) return httpError(403, `Le compte ${ownerLabel} n'a pas accès au plugin ${processing.plugin}.`) + if (status === 404) return httpError(404, `Le plugin ${processing.plugin} est introuvable dans le registre.`) + return httpError(502, 'Le registre des plugins est temporairement indisponible.') +} + async function ensurePluginAndReadSchema (processing: Pick) { - const account = { - type: processing.owner.type, - id: processing.owner.id, - ...(processing.owner.department ? { department: processing.owner.department } : {}) - } + const account = registryAccount(processing.owner) // `processing.plugin` is the registry artefact id, passed through as-is. // lib-node downloads + extracts the tarball into registryCacheDir on cache // miss; the cache is invalidated when the artefact's dataUpdatedAt bumps. - // - // Axios errors carry `status` but `lib-express/error-handler.js` collapses - // every AxiosRequestError to 500 — so we translate at the boundary here. let ensured try { ensured = await ensureArtefact({ @@ -68,15 +87,7 @@ async function ensurePluginAndReadSchema (processing: Pick | undefined @@ -92,6 +103,28 @@ async function ensurePluginAndReadSchema (processing: Pick) { + const ax = axiosBuilder({ + baseURL: config.privateRegistryUrl, + headers: { + 'x-secret-key': config.secretKeys.registry, + 'x-account': JSON.stringify(registryAccount(processing.owner)) + } + }) + try { + const res = await ax.get(`/api/v1/artefacts/${encodeURIComponent(processing.plugin)}`) + return res.data + } catch (err) { + throw translateRegistryError(err, processing) + } +} + const sensitiveParts = ['permissions', 'webhookKey', 'config'] /** @@ -466,6 +499,20 @@ router.get('/:id/plugin-config-schema', async (req, res, next) => { } catch (err) { next(err) } }) +// Get the plugin's registry metadata for this processing. Permission is checked +// against the requesting user; the registry fetch runs as the owner so a viewer +// with only an individual permission inherits the owner's plugin access. +router.get('/:id/plugin', async (req, res, next) => { + try { + const sessionState = await session.reqAuthenticated(req) + const processing = await mongo.processings.findOne({ _id: req.params.id }) + if (!processing) return res.status(404).send() + if (!['admin', 'exec', 'read'].includes(permissions.getUserResourceProfile(processing.owner, processing.permissions, sessionState) ?? '')) return res.status(403).send() + const artefact = await fetchPluginArtefact(processing) + res.status(200).json(artefact) + } catch (err) { next(err) } +}) + // Delete a processing router.delete('/:id', async (req, res) => { const sessionState = await session.reqAuthenticated(req) diff --git a/tests/features/processings/plugin-info-transitivity.api.spec.ts b/tests/features/processings/plugin-info-transitivity.api.spec.ts new file mode 100644 index 0000000..1ff1fb8 --- /dev/null +++ b/tests/features/processings/plugin-info-transitivity.api.spec.ts @@ -0,0 +1,81 @@ +import { test, expect } from '@playwright/test' +import { axiosAuth, clean } from '../../support/axios.ts' +import { publishFixturePlugin } from '../../support/registry.ts' + +/** + * A user with an *individual* read permission on a processing (not via + * ownership) must be able to read the plugin metadata even when they have no + * personal grant on the (private) plugin. The endpoint checks the processing + * permission against the user, then fetches the artefact from the registry on + * behalf of the OWNER (who does have the grant). + */ +test.describe('plugin info permission transitivity', () => { + test.beforeEach(clean) + test.afterAll(clean) + + test('individually-permitted user reads plugin metadata fetched as the owner', async () => { + // Private plugin: only test_org1 (the owner) is on privateAccess. The + // fixture auto-grants privateAccess accounts + superadmin; test_alone has + // NO grant of its own — that's the whole point. + const fixture = await publishFixturePlugin({ + name: '@data-fair/processing-hello-world', + version: '1.2.2', + isPublic: false, + privateAccess: [{ type: 'organization', id: 'test_org1', name: 'Test Org 1' }] + }) + + const adminTestOrg1 = await axiosAuth({ email: 'test_admin1@test.com', org: 'test_org1' }) + const aloneOutsider = await axiosAuth('test_alone@test.com') + + const processing = (await adminTestOrg1.post('/api/v1/processings', { + title: 'Transitive plugin read', + plugin: fixture.pluginId, + owner: { type: 'organization', id: 'test_org1', name: 'Test Org 1' } + })).data + + // Before the grant, the outsider cannot even see the processing. + await expect(aloneOutsider.get(`/api/v1/processings/${processing._id}/plugin`)) + .rejects.toMatchObject({ status: 403 }) + + // Grant test_alone an individual read permission on the processing. + await adminTestOrg1.patch(`/api/v1/processings/${processing._id}`, { + permissions: [{ profile: 'read', target: { type: 'userEmail', email: 'test_alone@test.com' } }] + }) + + // Now the outsider reads the plugin metadata — fetched as the owner. + const res = await aloneOutsider.get(`/api/v1/processings/${processing._id}/plugin`) + expect(res.status).toBe(200) + expect(res.data._id).toBe(fixture.pluginId) + }) + + test('owner lacking the plugin grant gets a translated 403/404', async () => { + // Owner test_user1 has the global access-grant but is NOT on privateAccess, + // so the owner-scoped registry fetch is denied. + const fixture = await publishFixturePlugin({ + name: '@data-fair/processing-hello-world', + version: '1.2.2', + isPublic: false, + privateAccess: [{ type: 'organization', id: 'test_org1', name: 'Test Org 1' }], + grants: [ + { type: 'user', id: 'test_superadmin' }, + { type: 'user', id: 'test_user1' }, + { type: 'organization', id: 'test_org1' } + ] + }) + const superadmin = await axiosAuth('test_superadmin@test.com') + + const processing = (await superadmin.post('/api/v1/processings', { + title: 'Owner has no access', + plugin: fixture.pluginId, + owner: { type: 'user', id: 'test_user1', name: 'Test User 1' } + })).data + + const res = await superadmin.get( + `/api/v1/processings/${processing._id}/plugin`, + { validateStatus: () => true } + ) + expect([403, 404]).toContain(res.status) + const body = typeof res.data === 'string' ? res.data : JSON.stringify(res.data) + expect(body).toContain(fixture.pluginId) + }) +}) From f0baec8808c9817c22b65993449a77ce4f62f131 Mon Sep 17 00:00:00 2001 From: Alban Mouton Date: Mon, 1 Jun 2026 14:52:50 +0200 Subject: [PATCH 3/6] feat(ui): fetch plugin metadata through the processing API Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/processing/processing-card.vue | 2 +- ui/src/composables/use-plugin-fetch.ts | 28 ++++++++++--------- ui/src/pages/processings/[id]/index.vue | 10 ++++--- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/ui/src/components/processing/processing-card.vue b/ui/src/components/processing/processing-card.vue index f9d2069..2d6284b 100644 --- a/ui/src/components/processing/processing-card.vue +++ b/ui/src/components/processing/processing-card.vue @@ -191,7 +191,7 @@ const props = defineProps({ showOwner: Boolean }) -const pluginFetch = usePluginFetch(props.processing.plugin) +const pluginFetch = usePluginFetch(props.processing._id) diff --git a/ui/src/composables/use-plugin-fetch.ts b/ui/src/composables/use-plugin-fetch.ts index c64b030..0889455 100644 --- a/ui/src/composables/use-plugin-fetch.ts +++ b/ui/src/composables/use-plugin-fetch.ts @@ -1,5 +1,7 @@ +import { $apiPath } from '../context' + // Subset of registry's Artefact shape that the UI uses. The artefact `_id` is -// exactly what we store on `processing.plugin`, so we fetch the artefact by that. +// exactly what we store on `processing.plugin`. export interface RegistryArtefact { _id: string name: string @@ -16,24 +18,24 @@ export interface RegistryArtefact { const fetches: Record>> = {} /** - * Fetch artefact metadata from the registry for a processing's `plugin` - * (the registry artefact id). + * Fetch a processing's plugin metadata through the processings API + * (`GET /processings/:id/plugin`). The API checks the caller's permission on + * the processing, then fetches the artefact from the registry as the owner — + * so a user with only an individual permission still sees the plugin even + * without a personal registry grant. * * Errors (404 deleted, 403 no access) are NOT broadcast as a global ui - * notification — callers read `error.value` and render their own inline - * state. See processing-card.vue and pages/processings/[id]/index.vue. - * - * Same-domain assumption: registry is always mounted at `/registry` of the - * current domain. The session cookie is sent automatically. + * notification — callers read `error.value` and render their own inline state. + * See processing-card.vue and pages/processings/[id]/index.vue. */ -export const usePluginFetch = (pluginId: string) => { - if (!fetches[pluginId]) { - fetches[pluginId] = useFetch( - `/registry/api/v1/artefacts/${encodeURIComponent(pluginId)}`, +export const usePluginFetch = (processingId: string) => { + if (!fetches[processingId]) { + fetches[processingId] = useFetch( + `${$apiPath}/processings/${encodeURIComponent(processingId)}/plugin`, { notifError: false } ) } - return fetches[pluginId] + return fetches[processingId] } export default usePluginFetch diff --git a/ui/src/pages/processings/[id]/index.vue b/ui/src/pages/processings/[id]/index.vue index 6fdb299..ed83f1d 100644 --- a/ui/src/pages/processings/[id]/index.vue +++ b/ui/src/pages/processings/[id]/index.vue @@ -148,13 +148,15 @@ async function fetchProcessing () { - configSchema null: legitimate "this plugin ships no schema" — distinct from a registry error and does NOT trigger the banner. - Same-domain assumption: registry is mounted at `/registry` of the current - domain, so an absolute path bypasses `$fetch`'s `/processings/api/v1` - baseURL (which would rewrite `/registry/...` to a 404). + Plugin metadata now flows through the processings API + (`/processings/:id/plugin`) rather than a direct same-origin `/registry` + call: the API gates on the caller's processing permission, then fetches the + artefact as the owner. This lets individually-permitted viewers see the + plugin without their own registry grant. */ const pluginFetch = useFetch( computed(() => processing.value?.plugin - ? `/registry/api/v1/artefacts/${encodeURIComponent(processing.value.plugin)}` + ? `${$apiPath}/processings/${processingId}/plugin` : null), { notifError: false } ) From beb8f8e9a19b1ffdfda03f72f5e88fd3c068b856 Mon Sep 17 00:00:00 2001 From: Alban Mouton Date: Mon, 1 Jun 2026 14:53:16 +0200 Subject: [PATCH 4/6] docs: note plugin-metadata-through-processing-API permission rule Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/architecture/v6-registry-integration.md | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/architecture/v6-registry-integration.md b/docs/architecture/v6-registry-integration.md index a270c06..5088d74 100644 --- a/docs/architecture/v6-registry-integration.md +++ b/docs/architecture/v6-registry-integration.md @@ -187,6 +187,29 @@ For v6.0, list plugins flat with a search box. No sub-categories. Same-domain deployment: registry sits on the same host at `/registry`, so the browser call to `/registry/api/v1/artefacts?...` is same-origin. No CORS configuration is required and the existing SimpleDirectory session cookie is naturally sent. The dev environment must mirror this — see E.1. +#### C.x Plugin metadata under processing permission + +The picker above (a creation flow for owners/admins) keeps its direct +same-origin `/registry/api/v1/artefacts` list call. But **viewing an existing +processing** must not require the viewer to have their own registry grant: a +user can hold an individual `read`/`exec` permission on a processing they don't +own. So plugin **metadata** for an existing processing is fetched through the +processings API, not directly from `/registry`: + +- `GET /api/v1/processings/:id/plugin` — gates on the caller's processing + permission (`getUserResourceProfile` ∈ `{admin, exec, read}`), then fetches + `GET /api/v1/artefacts/:pluginId` from the private registry with + `x-account: `. Registry access is therefore evaluated + against the **owner**, mirroring `/:id/plugin-config-schema`. Registry + 403/404 are translated and passed through (the UI shows its "plugin broken" + banner on those); other failures become 502. +- UI: `pages/processings/[id]/index.vue` and `usePluginFetch` + (`processing-card.vue`) call this endpoint instead of `/registry/...`. + +Thumbnails are still only rendered in the picker (`new.vue`), so no +processing-scoped thumbnail proxy exists yet; if a thumbnail is ever shown on a +permitted-but-ungranted viewer's card/detail it would need the same treatment. + #### C.2 Drop admin pages and nav - Delete `ui/src/pages/admin/plugins.vue` and matching route in `ui/src/router/`. - Drop the `/admin/plugins` nav entry. From 9ce0ce686b25af96fb934e91cfa69b162e12c4e9 Mon Sep 17 00:00:00 2001 From: Alban Mouton Date: Mon, 1 Jun 2026 15:01:28 +0200 Subject: [PATCH 5/6] chore: remove superpowers spec doc Co-Authored-By: Claude Opus 4.8 (1M context) --- ...gin-info-permission-transitivity-design.md | 106 ------------------ 1 file changed, 106 deletions(-) delete mode 100644 docs/superpowers/specs/2026-06-01-plugin-info-permission-transitivity-design.md diff --git a/docs/superpowers/specs/2026-06-01-plugin-info-permission-transitivity-design.md b/docs/superpowers/specs/2026-06-01-plugin-info-permission-transitivity-design.md deleted file mode 100644 index 2666d73..0000000 --- a/docs/superpowers/specs/2026-06-01-plugin-info-permission-transitivity-design.md +++ /dev/null @@ -1,106 +0,0 @@ -# Plugin info through the processing's permission boundary - -**Date:** 2026-06-01 -**Branch:** `fix-permission-transitivity` -**Status:** Approved design - -## Problem - -A user can be granted an *individual* permission (`read` / `exec` / `admin` via -the processing's `permissions` array) on a processing they do not own. Such a -user can open the processing, but the UI fetches the associated plugin's -metadata by calling the registry **directly from the browser**, authenticated -as that user: - -``` -GET /registry/api/v1/artefacts/:pluginId (ui/src/pages/processings/[id]/index.vue:157) -GET /registry/api/v1/artefacts/:pluginId (ui/src/composables/use-plugin-fetch.ts, used by processing-card.vue) -``` - -When the plugin is private and the **user** has no personal grant on it, the -registry returns 403/404. The UI interprets that as `pluginBroken`, shows the -"plugin broken" banner and suppresses the config form — even though the user is -legitimately allowed to see the processing. - -The registry access *should* be evaluated against the processing's **owner** -(who, by construction, has a working grant — the processing runs as them), not -against the individual viewer. The existing `GET /:id/plugin-config-schema` -endpoint already does exactly this: it gates on the processing permission of the -**user**, then calls the registry on behalf of the **owner** -(`ensurePluginAndReadSchema` → `ensureArtefact({ account: processing.owner })`). -Only the metadata fetch was left going direct. - -## Fix - -Route plugin **metadata** through the processing API under the same -"processing-permission-on-the-user, then registry-grant-on-the-owner" rule, and -repoint both UI surfaces at it. - -### API — new endpoint - -`GET /api/v1/processings/:id/plugin` in `api/src/processings/router.ts`: - -1. `session.reqAuthenticated(req)`, load the processing (404 if missing). -2. Gate identically to `/plugin-config-schema`: - `getUserResourceProfile(owner, permissions, sessionState)` ∈ `{admin, exec, read}` - → else 403. **Permission evaluated against the user.** -3. Fetch artefact metadata from `config.privateRegistryUrl`: - `GET /api/v1/artefacts/:pluginId` with headers - `x-secret-key: config.secretKeys.registry` and - `x-account: JSON.stringify(owner)`. **Registry grant evaluated against the - owner.** A lightweight authenticated GET — *not* `ensureArtefact`, which - downloads and extracts the whole tarball (too heavy for metadata). -4. Return the artefact JSON. Translate registry errors at the boundary the same - way `ensurePluginAndReadSchema` does: 403 → 403, 404 → 404 (so the UI's - `pluginBroken` still fires for genuinely revoked/deleted plugins), anything - else → 502. - -Refactor: extract the shared registry-header construction -(`{ 'x-secret-key', 'x-account': JSON.stringify(account) }`) and the -status-translation so the new endpoint and `ensurePluginAndReadSchema` don't -drift apart. - -### UI — repoint both surfaces - -- `ui/src/pages/processings/[id]/index.vue` (~L155): change the `useFetch` URL - from `/registry/api/v1/artefacts/:id` to - `${$apiPath}/processings/${processingId}/plugin`. -- `ui/src/composables/use-plugin-fetch.ts`: change `usePluginFetch` to take the - **processing id** (cache keyed by it) and fetch - `${apiPath}/processings/:id/plugin`. The returned `RegistryArtefact` shape is - unchanged, so consumers need no further change. -- `ui/src/components/processing/processing-card.vue` (~L194): pass the - processing id to the widened `usePluginFetch`. - -The plugin-**picker** flows (`ui/src/pages/processings/new.vue`, -`ui/src/components/processings-actions.vue`) keep their direct -`/registry/api/v1/artefacts` list calls — those are owner/admin creation -contexts where the user already has registry access. Out of scope. - -## Tests - -API test under `tests/features/processings/` (alongside -`plugin-access.api.spec.ts`): - -- **Regression target:** a user with an individual `read` permission on a - processing whose **owner** has plugin access — but who has **no** personal - grant — gets `200` from `GET /:id/plugin`. -- No permission on the processing → 403. -- Owner itself lacks the plugin grant → 403. -- Unknown / deleted plugin → 404. - -## Docs - -- Update `docs/architecture/v6-registry-integration.md`: the section describing - the UI fetching plugin metadata same-origin from `/registry` is now partly - superseded — plugin **metadata** flows through the processing API under the - processing-permission-then-owner-grant rule. Note this explicitly. - -## Out of scope / known follow-up - -- **Thumbnails.** Plugin thumbnails are only rendered in the picker - (`new.vue:75`, owner/admin context). The list cards and detail page carry the - `thumbnail` field in the type but do not display it, so there is nothing to - forward today. If a thumbnail is ever shown on a permitted-but-ungranted - user's card/detail, it would 403 the same way and need an equivalent - processing-scoped proxy. Noted, not built. From 47c8231bc20b37cf5900e09f22c1b02b022f9571 Mon Sep 17 00:00:00 2001 From: Alban Mouton Date: Mon, 1 Jun 2026 15:32:11 +0200 Subject: [PATCH 6/6] chore: upgrade agent skill --- .agents/skills/pr-ready/SKILL.md | 2 ++ skills-lock.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.agents/skills/pr-ready/SKILL.md b/.agents/skills/pr-ready/SKILL.md index 97ea79a..5482e8e 100644 --- a/.agents/skills/pr-ready/SKILL.md +++ b/.agents/skills/pr-ready/SKILL.md @@ -133,6 +133,8 @@ Required content: Length guidance: as compact as the change allows. A typo fix is one sentence. A multi-part feature is a short summary plus a tight bullet list. **No filler, no test-plan boilerplate, no marketing tone.** If you find yourself padding, stop. +Line breaks: **no superfluous line breaks.** Only insert a blank line where it marks a genuine section delimitation (e.g. between the summary and a `**Why:**` block, or before a bullet list). Do not separate every sentence with a blank line, do not pad between bullets, and do not add leading or trailing blank lines. + Example, for a small feature: ```markdown diff --git a/skills-lock.json b/skills-lock.json index 4fccda3..802734e 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -5,7 +5,7 @@ "source": "data-fair/lib", "sourceType": "github", "skillPath": "skills/pr-ready/SKILL.md", - "computedHash": "dd62e51657c77f3f44972b9ee62d3082a0503b3deb10f40d9fc64b45cd778451" + "computedHash": "88ee2f1b52748972924a4c8755be1be39916005a08cf97e6aa3b3076d38e27b3" }, "upgrade-scripts": { "source": "data-fair/lib",