diff --git a/.gitignore b/.gitignore index aab04ee..2122c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,9 +16,6 @@ test-report.junit.xml .env .env.* !.env.example -# Checked in like the examples above: it configures `dev:solo`, whose whole point is that it holds -# nothing worth keeping out of the repository. -!.env.solo .code-zero/ .data/ *.log @@ -32,4 +29,3 @@ test-report.junit.xml /vscode_cli.tar.gz code-zero.deployment.yml !code-zero.deployment.example.yml -!apps/dashboard/code-zero.deployment.solo.yml diff --git a/AGENTS.md b/AGENTS.md index f7b6ec0..c6a71b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -85,7 +85,6 @@ cp apps/dashboard/.env.example apps/dashboard/.env ```bash aube run dev # watch workspace development tasks -aube run dev:solo # dashboard alone, no database (apps/dashboard/.env.solo) aube run dev:docs # docs site alone aube run dev:marketing # marketing site alone aube run zero doctor # inspect the local environment diff --git a/README.md b/README.md index 4a08747..c442ce7 100644 --- a/README.md +++ b/README.md @@ -103,22 +103,26 @@ aube run dev The root `.env` configures the CLI. Each app loads its own file: the dashboard uses `apps/dashboard/.env`, while the docs app optionally uses `apps/docs/.env` for `NUXT_APP_BASE_URL`. -To see the dashboard before configuring anything, start it on its own instead: +The dashboard needs Postgres. It stores accounts, sessions, and the repositories a task may +target, so point `DATABASE_URL` in `apps/dashboard/.env` at a database and apply the schema before +the first start — there is no in-memory mode to fall back on: ```bash -mise install -aube ci -aube run dev:solo # http://localhost:3000, then sign up at /signup +aube run db:migrate +aube run dev # http://localhost:3000, then sign up at /signup +``` + +Set `AUTH_ENABLE_SIGNUP=true` in `apps/dashboard/.env` to create the first account, then turn it +back off. Self-registration grants the `user` role, not `admin`, and configuring repositories +(`repositories.save`, reached from the dashboard) requires `admin` — the same gate `audit.list` +uses — so promote that first account once, directly in the database: + +```sql +UPDATE "user" SET role = 'admin' WHERE email = 'you@example.com'; ``` -`dev:solo` is `nuxt dev` reading [`apps/dashboard/.env.solo`](./apps/dashboard/.env.solo) in -place of `.env`: Better Auth runs on an in-memory store, so there is no Postgres to install and no -migration to apply, and the account you create lives until you stop the process. Nothing else -about the app changes — it is the same UI, the same router, and the same authentication endpoints -a deployment serves. Tasks still need a checkout to target, so add one to -`CODE_ZERO_SOLO_REPOSITORIES` in that file; `observe` runs no model, so a task can be -created and inspected without a provider credential. Use `aube run dev` and `apps/dashboard/.env` -for anything that has to persist. +From there, add a repository from the dashboard to give tasks a checkout to target; `observe` +runs no model, so a task can be created and inspected without a provider credential. `dev:docs` and `dev:marketing` start those two apps on their own the same way `mail:preview` already does for `apps/mail-preview` — a plain `turbo run dev` filtered to one app, with no diff --git a/apps/dashboard/.env.solo b/apps/dashboard/.env.solo deleted file mode 100644 index c6cc921..0000000 --- a/apps/dashboard/.env.solo +++ /dev/null @@ -1,36 +0,0 @@ -# Environment for `aube run dev:solo` — the dashboard on its own, with no Postgres and no model -# credentials. Loaded only by that script (`nuxt dev --dotenv .env.solo`); `nuxt dev`, `nuxt build`, -# and a deployment all keep reading `.env`, which this file never touches. -# -# Checked in on purpose: it holds no secret worth keeping. The session store is in memory, so the -# signing secret below protects nothing that outlives the process, and the control-plane token is -# only accepted by a server started this way. -# -# Edit CODE_ZERO_SOLO_REPOSITORIES to point at a checkout you want to run tasks against. - -# Better Auth runs on an in-memory store, so the dashboard starts without a database. Same flag the -# Playwright preview server sets (see playwright.config.ts); `server/auth.config.ts` documents why -# it is not guarded by NODE_ENV. Never set it in `.env` or in a deployment's environment. -AUTH_E2E_MEMORY=true -# Nothing is ever dialed once the memory adapter replaces the database, but the options still have -# to build, so the resolver needs a value here. -DATABASE_URL=postgres://solo:solo@127.0.0.1:5432/solo -# Sessions live and die with the process, so this is a fixed placeholder rather than a real secret. -NUXT_BETTER_AUTH_SECRET=dev-only-solo-secret-not-used-in-any-deployment -# The store is empty at every start, so the first thing to do is create an account. -AUTH_ENABLE_SIGNUP=true - -# Control plane. `observe` runs no model, so a task can be created and inspected without any -# provider credential; `suggest` and `fix` need one from `.env`. -CODE_ZERO_CONTROL_PLANE_TOKENS=solo:solo -# What the `solo` token may run is policy, so it comes from the deployment configuration rather -# than from here: `code-zero.deployment.solo.yml`, named by CODE_ZERO_CONFIG below. -CODE_ZERO_CONFIG=code-zero.deployment.solo.yml - -# The repository table is in memory too and starts empty on every boot, and the procedures that -# would fill it require an administrator, which a freshly created throwaway account is not — so -# this seeds it. Read only when AUTH_E2E_MEMORY is true; a real deployment configures repositories -# from the dashboard instead. Entries are `owner/name=/path` (watched by the poller) or bare -# `/path` (allow-listed only), comma-separated. Empty means every task creation is refused, which -# is the safe default; add your own checkout to run one. -CODE_ZERO_SOLO_REPOSITORIES= diff --git a/apps/dashboard/code-zero.deployment.solo.yml b/apps/dashboard/code-zero.deployment.solo.yml deleted file mode 100644 index 78a8794..0000000 --- a/apps/dashboard/code-zero.deployment.solo.yml +++ /dev/null @@ -1,9 +0,0 @@ -# Deployment policy for `aube run dev:solo`, named by CODE_ZERO_CONFIG in .env.solo. -# -# Checked in like .env.solo and for the same reason: it holds no secret. The token this grants -# modes to is `solo:solo`, which only a server started this way accepts. -control_plane: - # `observe` runs no model, so a task can be created and inspected without any provider - # credential; `suggest` and `fix` need one from .env. - modes: - solo: [observe, suggest, fix] diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 0702ce7..4dd5710 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -6,7 +6,6 @@ "build": "nuxt build", "clean": "nuxt cleanup && tsc -b --clean", "dev": "nuxt dev", - "dev:solo": "nuxt dev --dotenv .env.solo", "lint": "oxlint --config ../../tooling/oxc/apps.oxlintrc.json --type-aware --type-check --ignore-pattern \"test/nuxt/components/**\" --ignore-pattern \"test/nuxt/pages/**\" app config modules server test", "lint:fix": "oxlint --fix --config ../../tooling/oxc/apps.oxlintrc.json --type-aware --type-check --ignore-pattern \"test/nuxt/components/**\" --ignore-pattern \"test/nuxt/pages/**\" app config modules server test", "prelint": "nuxt prepare", diff --git a/apps/dashboard/server/auth.config.ts b/apps/dashboard/server/auth.config.ts index 42ae0b2..7ba7049 100644 --- a/apps/dashboard/server/auth.config.ts +++ b/apps/dashboard/server/auth.config.ts @@ -75,22 +75,23 @@ const options = authBetterAuthOptions({ }); /** - * `AUTH_E2E_MEMORY` swaps the Postgres adapter for an in-memory one. Two callers set it, both of - * which own the whole server process and throw its store away when they exit: the Playwright - * preview server (`start:playwright:webserver`, see `playwright.config.ts`), so the e2e suite in + * `AUTH_E2E_MEMORY` swaps the Postgres adapter for an in-memory one. One caller sets it, and it + * owns the whole server process and throws its store away when it exits: the Playwright preview + * server (`start:playwright:webserver`, see `playwright.config.ts`), so the e2e suite in * `test/e2e/test-utils.ts` can sign up and sign in its own throwaway account through the real - * `/api/auth/**` endpoints without a live database; and `dev:solo` (`.env.solo`), so the - * dashboard starts from a fresh clone without one either. Both stay off the network and off mutable + * `/api/auth/**` endpoints without a live database. It stays off the network and off mutable * external state. `AUTH_DATABASE_URL` still has to resolve to build `options` above, but nothing * ever queries it once `database` is overridden here. * + * Running the dashboard itself is not one of those callers: `aube run dev` and a deployment alike + * need Postgres, and there is no env file in the repository that turns this on. + * * Deliberately not guarded by `NODE_ENV`: `nuxt preview` — the command this app's own e2e suite * runs, per `start:playwright:webserver` above — sets `NODE_ENV=production` whenever it isn't * already set (`@nuxt/cli`'s `preview` command), identically to a real deployment's built output. * A `NODE_ENV === 'production'` check would therefore reject every e2e run, not just a leaked - * flag. Keep this variable out of any shared `.env`/CI template that a real deployment also reads — - * `.env.solo` is not one: `nuxt` loads it only when a command names it with `--dotenv`, which is - * how `dev:solo` alone reaches it. + * flag. Keep this variable out of every `.env`/CI template a real deployment also reads; + * `playwright.config.ts` passes it to the one server it starts and nowhere else. */ export default defineServerAuth( process.env.AUTH_E2E_MEMORY === 'true' diff --git a/apps/dashboard/server/utils/repositories.ts b/apps/dashboard/server/utils/repositories.ts index 2dcf90f..ae32b41 100644 --- a/apps/dashboard/server/utils/repositories.ts +++ b/apps/dashboard/server/utils/repositories.ts @@ -1,5 +1,3 @@ -import { resolve } from 'node:path'; - import { deleteRepository, isAllowedCheckout, @@ -16,10 +14,10 @@ import { database } from './database.js'; /** * The repositories this deployment may act on. * - * A contract rather than the Drizzle functions directly, so the process can run on an in-memory - * store when it has no database — the same shape `server/auth.config.ts` takes for the session - * store, and for the same reason: `dev:solo` and the Playwright preview both own the whole process - * and throw its state away when they exit. + * A contract rather than the Drizzle functions directly, so the Playwright preview server can run + * on an in-memory store — the same shape `server/auth.config.ts` takes for the session store, and + * for the same reason: that process owns its whole state and throws it away when it exits. Every + * other process, a deployment and `aube run dev` alike, reads Postgres. */ export interface RepositoryStore { list(): Promise; @@ -43,18 +41,13 @@ function postgresRepositoryStore(): RepositoryStore { } /** - * The in-memory stand-in, for a process running without a database. - * - * Seeded from `CODE_ZERO_SOLO_REPOSITORIES` because the store starts empty on every boot and the - * procedures that would fill it require an administrator, which a freshly created throwaway - * account is not. That variable is read here and nowhere else: it configures a fixture, not a - * deployment, which is why the three variables this table replaced are gone rather than joined by - * a fourth. + * The in-memory stand-in, for the Playwright preview server, which runs without a database. * - * Entries are `owner/name=/path` or bare `/path`; the first form is watched, the second is only - * allow-listed. A malformed entry is dropped, the same way the poller's own configuration was. + * Starts empty and stays in the process: the e2e suite creates whatever it needs through the same + * procedures an operator uses, and nothing seeds it from the environment, because a store that + * outlives no process is a fixture rather than a deployment's configuration. */ -export function memoryRepositoryStore(seed: string | undefined): RepositoryStore { +export function memoryRepositoryStore(): RepositoryStore { const records = new Map(); let sequence = 0; @@ -85,24 +78,6 @@ export function memoryRepositoryStore(seed: string | undefined): RepositoryStore return record; }; - for (const entry of (seed ?? '').split(',')) { - const trimmed = entry.trim(); - if (trimmed === '') continue; - const [slug, checkoutPath] = trimmed.includes('=') - ? trimmed.split('=', 2).map((part) => part.trim()) - : [undefined, trimmed]; - if (!checkoutPath) continue; - const [owner, name] = (slug ?? '').split('/', 2).map((part) => part.trim()); - put({ - // Resolved the same way `mayTargetRepository` resolves the path a task creation names - // (`context.ts`) and `repositories.save` resolves an operator-supplied one (`router.ts`): - // a relative or trailing-slash entry here must still string-equal what a task creation - // compares it against, or every task creation against it is refused as not allow-listed. - checkoutPath: resolve(checkoutPath), - ...(owner && name ? { owner, name, pollEnabled: true } : {}), - }); - } - return { list: () => Promise.resolve([...records.values()]), watched: () => @@ -123,9 +98,8 @@ export function memoryRepositoryStore(seed: string | undefined): RepositoryStore * The store this process uses. * * `AUTH_E2E_MEMORY` selects the in-memory one, the same flag `server/auth.config.ts` reads: it - * marks a process whose stores live and die with it, which is true of both stores or neither. + * marks a process whose stores live and die with it, which is true of both stores or neither. Only + * `playwright.config.ts` sets it, so everything else here talks to Postgres. */ export const repositoryStore: RepositoryStore = - process.env.AUTH_E2E_MEMORY === 'true' - ? memoryRepositoryStore(process.env.CODE_ZERO_SOLO_REPOSITORIES) - : postgresRepositoryStore(); + process.env.AUTH_E2E_MEMORY === 'true' ? memoryRepositoryStore() : postgresRepositoryStore(); diff --git a/apps/dashboard/test/unit/repositories.test.ts b/apps/dashboard/test/unit/repositories.test.ts index dde8d06..2456f53 100644 --- a/apps/dashboard/test/unit/repositories.test.ts +++ b/apps/dashboard/test/unit/repositories.test.ts @@ -7,8 +7,18 @@ import { memoryRepositoryStore } from '../../server/utils/repositories.js'; const CHECKOUT = resolve('/srv/widget'); describe('memoryRepositoryStore', () => { - it('watches a seeded `owner/name=/path` entry and only allow-lists a bare path', async () => { - const store = memoryRepositoryStore(`acme/widget=${CHECKOUT},/srv/other`); + it('starts empty, so nothing is allow-listed until it is saved', async () => { + const store = memoryRepositoryStore(); + + expect(await store.list()).toEqual([]); + expect(await store.allows(CHECKOUT)).toBe(false); + }); + + it('watches a saved repository with coordinates and polling on, and only allow-lists a bare path', async () => { + const store = memoryRepositoryStore(); + + await store.save({ owner: 'acme', name: 'widget', checkoutPath: CHECKOUT, pollEnabled: true }); + await store.save({ checkoutPath: resolve('/srv/other') }); expect(await store.allows(CHECKOUT)).toBe(true); expect(await store.allows(resolve('/srv/other'))).toBe(true); @@ -18,7 +28,8 @@ describe('memoryRepositoryStore', () => { }); it('keeps the fields a save leaves out, the way the Postgres upsert does', async () => { - const store = memoryRepositoryStore(`acme/widget=${CHECKOUT}`); + const store = memoryRepositoryStore(); + await store.save({ owner: 'acme', name: 'widget', checkoutPath: CHECKOUT, pollEnabled: true }); const saved = await store.save({ checkoutPath: CHECKOUT }); @@ -36,7 +47,8 @@ describe('memoryRepositoryStore', () => { }); it('writes the fields a save does name, and defaults them for a repository it has never seen', async () => { - const store = memoryRepositoryStore(`acme/widget=${CHECKOUT}`); + const store = memoryRepositoryStore(); + await store.save({ owner: 'acme', name: 'widget', checkoutPath: CHECKOUT, pollEnabled: true }); const updated = await store.save({ checkoutPath: CHECKOUT, @@ -54,4 +66,13 @@ describe('memoryRepositoryStore', () => { pollEnabled: false, }); }); + + it('removes a record by id', async () => { + const store = memoryRepositoryStore(); + const saved = await store.save({ checkoutPath: CHECKOUT }); + + expect(await store.remove(saved.id)).toBe(true); + expect(await store.list()).toEqual([]); + expect(await store.remove(saved.id)).toBe(false); + }); }); diff --git a/docs/PLAN.md b/docs/PLAN.md index ba87b16..4d6d266 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -138,6 +138,15 @@ Fasi 0-5 eseguite. Cosa è cambiato rispetto al piano, e perché: Trovato strada facendo: l'allow-list dei repository esisteva solo se erano configurati anche i token operatore, quindi un deployment con sole sessioni non poteva creare nessun task. Corretto. +## Aggiornamento al 2026-09-12 + +`dev:solo` e `apps/dashboard/.env.solo` sono stati rimossi, insieme a +`code-zero.deployment.solo.yml` e a `CODE_ZERO_SOLO_REPOSITORIES`: la dashboard richiede Postgres, +come ogni deployment, e non esiste più un avvio senza database. L'adattatore Better Auth in memoria +resta, ma lo accende solo il server di anteprima di Playwright (`playwright.config.ts`), e lo store +dei repository in memoria parte vuoto. Le righe sopra che citano `dev:solo` restano come cronaca +della Fase 0, non come istruzioni. + ## Cosa resta | Cosa | Perché non è stato fatto | @@ -160,7 +169,7 @@ token operatore, quindi un deployment con sole sessioni non poteva creare nessun ## Rischi -- **Run lunghi dentro `nuxt dev`**: HMR riavvia Nitro e uccide il run. Mitigazione: `dev:solo` in +- **Run lunghi dentro `nuxt dev`**: HMR riavvia Nitro e uccide il run. Mitigazione: `aube run dev` in `observe`, run veri solo su `.output/` o con `nuxt dev --no-fork`. - **KV `fs-lite` senza scrittura atomica**: `list()` legge tutte le chiavi a ogni overview. Va bene fino a qualche migliaio di task; poi Postgres (già in repo per l'auth). diff --git a/package.json b/package.json index 68feeb4..18c3f77 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "dev": "turbo run dev", "dev:docs": "turbo run dev --filter=@code-zero/docs", "dev:marketing": "turbo run dev --filter=@code-zero/marketing", - "dev:solo": "turbo run dev:solo --filter=@code-zero/dashboard", "format": "oxfmt -c tooling/oxc/.oxfmtrc.json --ignore-path .oxfmtignore .", "format:check": "oxfmt -c tooling/oxc/.oxfmtrc.json --ignore-path .oxfmtignore --check .", "i18n:report": "turbo run i18n:report", diff --git a/turbo.jsonc b/turbo.jsonc index ff053e5..adb1e23 100644 --- a/turbo.jsonc +++ b/turbo.jsonc @@ -197,14 +197,6 @@ "cache": false, "persistent": true, }, - // The dashboard on its own, reading apps/dashboard/.env.solo instead of .env: no database, no - // model credentials, no other app in the graph. Same task shape as `dev`, since it is the same - // command with a different env file. - "dev:solo": { - "dependsOn": ["^build"], - "cache": false, - "persistent": true, - }, "clean": { "cache": false, },