Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion brain/canonico/CURRENT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The product is positioned as a documentation discovery platform for plugin ecosy
- Scheduled sync command for latest plugin versions.
- Reverb private channels by community for ingestion status and command index updates.
- Angular SSR shell with lazy standalone routes, Signals-based UI state, Transloco i18n, command palette, protected routes, and reusable UI components.
- Browser/SSR runtime configuration separates internal SSR API calls from public browser API, Reverb, allowed hosts, and canonical public origin.
- Browser/SSR runtime configuration separates internal SSR API calls from public browser API, Reverb, allowed hosts, and canonical public origin. Proven in the production stack: the hydrated browser calls only the configured public origin, `publicUrlFor()` in `frontend/src/server.ts` builds canonical and metadata from `COMMANDSPHERE_PUBLIC_ORIGIN` and ignores the request `Host`, and the browser websocket reaches the configured public Reverb host even though the `frontend` service still pins `COMMANDSPHERE_REVERB_HOST: localhost`.
- Markdown viewer sanitization and heading/code enhancement.
- Server-side HTML sanitization of `content_html` in two layers: allowlist sanitizer applied at ingestion and again through a `Document` accessor on every read, so stored documents are served sanitized without rewriting the column.
- Enforced Content-Security-Policy on every response. SSR HTML, prerendered documents included, gets a per-request `style-src` nonce with no `unsafe-inline` or `unsafe-eval` and `Cache-Control: no-store`; `connect-src` is built by the same function that serves `/runtime-config.js`. The API sends `default-src 'none'` from global middleware, error responses included.
Expand Down Expand Up @@ -102,6 +102,13 @@ Documented evidence in `README.md` and `docs/PROGRESS.md` says v1 has passed pha

Current Brain bootstrap did not rerun the full product gate set because this change only adds documentation. Future product changes must run relevant gates and record `VALIDATED` versus `PENDING`.

## Production Stack Facts

Confirmed on 2026-09-23 by running `docker-compose.prod.yml` with public values that differ from the defaults, twice and independently. The stack was built from commit `29a7881`, before the Content-Security-Policy work landed on `main`, so anything below about response headers describes that commit. Details in `brain/handoffs/2026-09-23-production-hydrated-smoke.md`.

- Seeding does not work in the production image. `fakerphp/faker` sits in `require-dev` in `backend/composer.json` while `docker/backend.prod.Dockerfile` installs with `composer install --no-dev`, so any factory calling `fake()` raises `Call to undefined function Database\Factories\fake()`. Migrations and Scout index sync work normally.
- The API answered `Access-Control-Allow-Origin: *`, the Laravel default with `config/cors.php` unpublished, so no origin is restricted. Confirm it on current `main` before acting on it.

## Known Constraints

- Real Discord OAuth requires operator-provided credentials.
Expand Down
29 changes: 26 additions & 3 deletions brain/canonico/NEXT_ACTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Risks:

Problem: The browser API base URL is hardcoded to `http://localhost:8000/api/v1`, while production Compose only configures the SSR server-side API URL. Reverb runtime config also relies on local defaults/localStorage.

Status: Implemented in branch `fix/runtime-production-config` as the first build-loop remediation phase. Keep the Docker production hydrated-browser smoke as a remaining validation item before treating this as fully production-proven.
Status: Implemented in branch `fix/runtime-production-config` and production-proven. The Docker production hydrated-browser smoke ran on 2026-09-23 against `docker-compose.prod.yml` with public values that differ from the defaults, under adversarial audit, and closed the last validation item. See `brain/handoffs/2026-09-23-production-hydrated-smoke.md`.

Recommended direction:

Expand All @@ -42,8 +42,10 @@ Acceptance criteria:

- Production-mode browser calls do not point to localhost. `VALIDATED` through SSR artifact smoke with public runtime config.
- SSR uses the configured production origin for canonical, Open Graph, and JSON-LD metadata. `VALIDATED` through SSR artifact smoke.
- Reverb uses documented production runtime config. `VALIDATED` at configuration level; websocket integration remains part of Docker/E2E validation.
- Docker production smoke validates hydrated browser API calls.
- Reverb uses documented production runtime config. `VALIDATED` in the production stack: the browser opens `ws://<public host>:<public port>/app/<key>` from the served runtime config, and Reverb answers `pusher:connection_established`. The public variable wins over the fixed `COMMANDSPHERE_REVERB_HOST: localhost` in the `frontend` service.
- Docker production smoke validates hydrated browser API calls. `VALIDATED`: 53 post-hydration requests, every API call on the configured origin, zero requests to the default origin, which was bound away and answered connection refused.
- SSR ignores a forged `Host` header for canonical and metadata. `VALIDATED`: `publicUrlFor()` in `frontend/src/server.ts` builds from `COMMANDSPHERE_PUBLIC_ORIGIN` only.
- Production `dev-login` stays disabled. `VALIDATED`: `403 auth.dev_login_disabled`, refused before request validation.

Risks:

Expand Down Expand Up @@ -268,8 +270,29 @@ Risks:

- An Angular major upgrade touches SSR, build and tests at once; it needs its own branch, gates and audit.

## Priority 13 - Seeding Is Broken In The Production Image

Problem: `fakerphp/faker` is declared only in `require-dev` in `backend/composer.json`, and `docker/backend.prod.Dockerfile` installs with `composer install --no-dev`. Every factory that calls `fake()`, starting at `backend/database/factories/UserFactory.php`, is therefore undefined in the production image, so `php artisan migrate --seed` and `php artisan db:seed` abort with `Call to undefined function Database\Factories\fake()`.

Reproduced twice and independently on 2026-09-23, in the smoke and in the audit.

Recommended direction:

- Decide whether the production image is supposed to seed at all. If it is, move `fakerphp/faker` to `require`, or split demo factories from the production-facing seeder so that the production path has no Faker dependency.
- Keep the split explicit, so a demo or portfolio dataset never becomes a production seeding requirement by accident.

Acceptance criteria:

- Seeding either succeeds in the `--no-dev` image or is explicitly documented as unsupported there, with the supported path written down.
- The production Docker smoke can create a dataset without direct SQL inserts.

Risks:

- Moving Faker to `require` ships a development library in the production image. Splitting the seeders is more work but keeps the image lean.

## Backlog

- Review the API `Access-Control-Allow-Origin: *`. The 2026-09-23 production smoke saw it on `/api/v1/auth/dev-login`, which means `config/cors.php` is unpublished and every origin is allowed. That smoke predates the CSP work merged in ADR-30, so confirm the header on current `main` before acting.
- Isolate the backend test suite from the development database. Feature tests use `RefreshDatabase`, `backend/phpunit.xml` keeps its `DB_CONNECTION`/`DB_DATABASE` overrides commented out, and no `backend/.env.testing` exists, so the documented Pest gate wipes `commandsphere` on every run. Point the suite at a dedicated test database, as CI already does with `commandsphere_test`, and document how to create it locally.
- Fix `portfolio-happy-paths.spec.ts` › `admin sincroniza e vê ingestion run`, the only failing E2E test: it asserts `/Run #/` while the pt-BR UI renders `Execução #` from the `runId` key in `frontend/public/i18n/pt-BR.json`.
- Make the Pest assertion total deterministic. `it indexes commands in meilisearch through scout import` makes between 8 and 10 assertions depending on the run, so the suite total varies on unchanged code; the test count is stable.
Expand Down
40 changes: 40 additions & 0 deletions brain/handoffs/2026-09-23-production-hydrated-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Handoff - Production Hydrated-Browser Smoke

Date: 2026-09-23
Branch: `claude/estruturar-agentes-20566a` (validation only, no product change)
Stack built from: `29a7881`, before the Content-Security-Policy work (ADR-30) landed on `main`

## Scope

Item C7: close the last open acceptance criterion of Priority 2, "Docker production smoke validates hydrated browser API calls". Nothing in the product was allowed to change; the item only had to prove that the hydrated browser uses the configured public origin instead of the `localhost` defaults.

## How It Was Validated

The stack ran twice, with two disjoint sets of public values, in throwaway Compose projects with their own volumes.

- First pass: `127.0.0.1:4200` as public origin, `127.0.0.1:8000/api/v1` as public API, Reverb on `127.0.0.1:8080`.
- Adversarial pass: `127.0.0.55:4373` as public origin, `127.0.0.55:8373/api/v1` as public API, Reverb on `127.0.0.55:8473`, with `COMMANDSPHERE_ALLOWED_HOSTS` matching. Ports were bound to `127.0.0.55` only, so the default origin answered connection refused and any leak would fail loudly instead of silently succeeding.

Secrets were generated per run into an env file outside the repository. No versioned file was touched in either pass; `git diff main` stayed empty.

## Proven

- Canonical, `og:url`, `og:image`, `twitter:image`, and JSON-LD carry the configured public origin on the landing page, a plugin page, and a command page. Zero occurrences of the default origin.
- `/runtime-config.js` serves `apiBaseUrl`, `publicOrigin`, and the Reverb block exactly as configured, with `Cache-Control: no-store`, and the served Reverb app key matches the configured one.
- After hydration, with hydration proven by a typed search that produced DOM results and a `routerLink` navigation that issued no new document request, 53 requests were captured and every one landed on a configured origin. API calls covered search, command detail, plugin detail, and plugin version documents.
- The browser websocket opens `ws://<configured host>:<configured port>/app/<key>` and Reverb answers `pusher:connection_established`. The public variable wins over the `COMMANDSPHERE_REVERB_HOST: localhost` pinned in the `frontend` service of `docker-compose.prod.yml`.
- A forged `Host` header (`evil.example.com`, `attacker.test`) never reaches canonical or metadata, because `publicUrlFor()` in `frontend/src/server.ts` builds URLs from `COMMANDSPHERE_PUBLIC_ORIGIN` alone.
- `POST /api/v1/auth/dev-login` answers `403 auth.dev_login_disabled` in production, and refuses before request validation.

## Remaining Work

- Seeding is broken in the production image: `fakerphp/faker` is a dev dependency and the image installs `--no-dev`. Tracked as Priority 13. The adversarial pass worked around it with direct SQL inserts into the throwaway volume, which is how the plugin and command pages could be tested at all.
- The stack carried no `Content-Security-Policy` header at `29a7881`, so the clean console proved nothing about policy. ADR-30 has since added CSP on `main`; this smoke says nothing about that implementation and does not validate it.
- The API served `Access-Control-Allow-Origin: *`. Tracked in the backlog, to be confirmed on current `main`.
- Every result here is evidence about `29a7881`. `main` has since changed `frontend/src/server.ts` and the backend middleware stack, so re-running this smoke is what would extend the proof to current `main`.

## Gates

- Production Docker runtime gate: `VALIDATED` in both passes, including migrations and Scout index sync.
- `docker compose ... migrate:fresh --seed`: `FAILED` for the reason above, not by regression.
- Backend and frontend unit, lint, and build gates: `PENDING`. This item changed no code, so they were not run.
Loading