diff --git a/plugins/EXTRACT-ATELIER-PROMPT.md b/plugins/EXTRACT-ATELIER-PROMPT.md
index d93adcf1..d2013e69 100644
--- a/plugins/EXTRACT-ATELIER-PROMPT.md
+++ b/plugins/EXTRACT-ATELIER-PROMPT.md
@@ -34,8 +34,8 @@ Everything below happens in `~/officer-dev/atelier-work`.
3. **`src/servers/plugins/`** — `discover.ts`, `manifest.ts`, `mount.ts`, `install.ts`, `ecosystem.ts`.
Where this prompt and that code disagree, **the code is right**.
4. **`CLAUDE.md`** — house rules, the permission model, and why `assertPermissionTotality` refuses to boot.
-5. **`plugins/gitea/`** — because of the UI decision below. It solves the same problem.
-6. **`docs/secret-store.md`** — Atelier holds an OpenAI key.
+5. **`docs/secret-store.md`** — Atelier holds an OpenAI key.
+6. **`src/workspaces/officerdev/APP_CONVENTIONS.md`** and `HOOK_CONVENTIONS.md` — you are writing panels.
## What Atelier is
@@ -52,8 +52,9 @@ env BUN_PORT, PUBLIC_URL, POSTGRES_URL, ATELIER_DATA_DIR,
runs as pm2 `officer-atelier`, cwd ~/officer-dev/dockers/officer-atelier
```
-It is a **sidecar** plugin, not an API-only one, for the two reasons that always decide this: it does
-long-running work with a concurrency cap, and it holds a credential the platform must not.
+It is a **sidecar** plugin for one reason that survives the UI moving into Officer: generation is
+long-running, capped by `ATELIER_MAX_CONCURRENCY`, and holds a credential the platform must not. The UI
+is Officer's; the work is the sidecar's.
## Decisions already made — do not re-open these
@@ -61,13 +62,35 @@ Each of these would normally be a question. They are answered so you can keep mo
evidence that one is wrong, **write it in `BLOCKERS.md`, follow the decision anyway, and carry on** — a
half-applied decision is worse than a wrong one consistently applied.
-**1. The UI: follow gitea, do not port it.** Atelier ships htmx _and_ a React SPA with its own build
-(`build-web.ts`, `gen-index.ts`). Rewriting 84 files into panels is not an overnight job and is not the
-point of this extraction. Instead: the sidecar keeps serving its own UI, the plugin proxies it
-same-origin with `createSidecarProxy`, and a single panel renders it in an iframe. `plugins/gitea` does
-exactly this and its `sidecar/rewrite.ts` records the traps. **Read that file before writing any of it.**
+**1. The UI is PORTED into Officer. No proxy, no iframe, no sidecar-served UI.** Atelier was a separate
+server because that was the quick answer at the time; this extraction is what undoes that. It becomes part
+of Officer, not something Officer frames.
-**2. The database: check the rows, then decide.** Atelier uses a separate `atelier` Postgres database,
+It is smaller than the repository suggests:
+
+```
+src/apps/frontend 1,736 lines / 25 files
+ 6 of those components/ui/* — shadcn Officer ALREADY HAS. Delete, import from @/components/ui.
+ real work Generate · Gallery (+ Lightbox) · Collections · Providers ≈1,200 lines
+src/apps/web the htmx half — 8 files, DELETED, not ported
+```
+
+So: three panel apps — **Generate**, **Gallery**, **Collections** — under
+`plugins/atelier/web/`, each exporting `appRegistryMetas`, with a `layout.ts` composing them. Follow the
+frontend route conventions in `CLAUDE.md`: selection lives in the URL, rows are real links, no
+`usePanelChannel` for "which thing is open".
+
+The fourth feature, **Providers**, is not a panel — see the next decision.
+
+**2. Providers become Officer settings, with more than one.** `features/Providers/` is a settings screen,
+and Officer has a settings surface already. It moves there rather than becoming a panel.
+
+`src/servers/providers/` is already adapter-shaped — `types.ts`, `openai.ts`, `credentials.ts`, an
+`index.ts` that dispatches. Keep that shape and keep the seam: today OpenAI is the only adapter, and the
+point of moving the settings is that a second one is a new file rather than a new screen. **Do not add a
+second provider in this run** — leave the shape able to take one and say so in `PLUGIN.md`.
+
+**3. The database: check the rows, then decide.** Atelier uses a separate `atelier` Postgres database,
deliberately — `src/databases/atelier_db/drizzle.config.ts` says why. Plugin tables normally live in
`officer_db` with a prefix.
@@ -78,17 +101,17 @@ deliberately — `src/databases/atelier_db/drizzle.config.ts` says why. Plugin t
- **If either has rows**, keep the separate database for now and record the migration as a follow-up in
`BLOCKERS.md`. Losing someone's generated images to a schema decision at 4am is not a trade worth making.
-**3. Artefacts: `$OFFICER_ROOT/data/atelier/`.** Generated images are files, and `ATELIER_DATA_DIR`
+**4. Artefacts: `$OFFICER_ROOT/data/atelier/`.** Generated images are files, and `ATELIER_DATA_DIR`
currently points wherever its `.env` says. A plugin directory is a git clone that an update replaces, so
artefacts kept there vanish on upgrade with nothing logging it. `plugins/kiosk/artefacts.ts` has the same
problem solved and the comment explaining it.
-**4. `OPENAI_API_KEY` goes in the secret store**, not `.env`. `putSecret`/`getSecret` landed 2026-08-19;
+**5. `OPENAI_API_KEY` goes in the secret store**, not `.env`. `putSecret`/`getSecret` landed 2026-08-19;
`bun run secret:set atelier-openai-key` puts it there. Read with `getSecret`, **never `getKey`** — `getKey`
mints a random value for an absent purpose, which for a foreign credential means silently authenticating
with a fake. Fall back to env when the store has nothing, so an install that predates this keeps working.
-**5. One permission, `atelier`, `kind: 'app'`.** Image generation is not `execution` — it does not run as
+**6. One permission, `atelier`, `kind: 'app'`.** Image generation is not `execution` — it does not run as
the owner's OS user in the owner's home.
## How to work
@@ -135,6 +158,11 @@ can be found later with a grep.
- No unauthenticated routes. A plugin cannot serve one; `UNPROTECTED_API_PREFIXES` is hardcoded in the
platform. If Atelier needs one, that is a blocker, not something to work around. (`plugins/kiosk`
is stuck on exactly this — read its README before concluding you have found a way.)
+- **Generated images are served through an authenticated route**, like every other plugin artefact. Every
+ image having "a permanent URL" was true of a standalone server with its own `PUBLIC_URL`; inside Officer
+ the URL is behind the session. If something needs an image without one — an `
` in an email, a
+ share link — that is a blocker to write down, not a route to open. See the image-rendering affordance
+ in `capabilities/affordances/officer/core/` for why an `
` cannot carry a token.
- No changes to `src/servers/plugins/` to make Atelier fit. If the contract does not accommodate it, that
is a finding worth more than the workaround.
- No second agent, no second worktree, no background job that writes to this tree.