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
8 changes: 6 additions & 2 deletions brain/canonico/CURRENT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,14 @@ Current Brain bootstrap did not rerun the full product gate set because this cha

## 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`.
Confirmed by running `docker-compose.prod.yml` with public values that differ from the defaults, four times in total and each time under an independent adversarial repeat. Two passes ran on `29a7881`, before Content-Security-Policy existed, and two on `c4eee46`, with CSP enforced. Every pass used a different public IP and port set and published ports on that IP only, so the default origin answered connection refused and a leak would fail loudly. Details in `brain/handoffs/2026-09-23-production-hydrated-smoke.md` and `brain/handoffs/2026-09-23-production-smoke-csp-recheck.md`.

- The hydrated browser calls only the configured public origin, canonical and metadata ignore a forged `Host` or `X-Forwarded-Host`, `/runtime-config.js` serves the configured values, the browser websocket reaches the configured public Reverb host, and production `dev-login` answers `403` before request validation. Proven on both commits.
- CSP is enforced, not merely announced. `connect-src` carries the configured API and Reverb origins and no defaults, the `style-src` nonce changes per response under `Cache-Control: no-store`, and browser probes on a served page confirm `disposition: enforce` against an inline style without nonce, an inline script, a foreign script, and a foreign `fetch`, while a style carrying the header nonce applies.
- Meilisearch index settings are applied by no code path. `filterableAttributes` stays empty until `php artisan scout:sync-index-settings` runs, including when documents arrive through the normal ingestion path with Scout observers and Horizon active. A fresh index therefore answers `500` with `Attribute community is not filterable` on faceted search until that command runs. It is the documented step in `README.md`, not a regression, and it must run before or after import on every new environment.
- 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.
- The API answers `Access-Control-Allow-Origin: *`, the Laravel default with `config/cors.php` unpublished, on every response including `403`, `404`, and `500`, and including a forged `Origin`. Confirmed on `c4eee46`.
- API responses carry duplicated security headers, sent by both nginx and the `SecurityHeaders` middleware.

## Known Constraints

Expand Down
6 changes: 4 additions & 2 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` 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`.
Status: Implemented in branch `fix/runtime-production-config` and production-proven, on `29a7881` and again on `c4eee46` with Content-Security-Policy enforced. Both rounds ran against `docker-compose.prod.yml` with public values that differ from the defaults and were repeated independently under adversarial audit. See `brain/handoffs/2026-09-23-production-hydrated-smoke.md` and `brain/handoffs/2026-09-23-production-smoke-csp-recheck.md`.

Recommended direction:

Expand Down Expand Up @@ -292,7 +292,9 @@ Risks:

## 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.
- Review the API `Access-Control-Allow-Origin: *`. `config/cors.php` is unpublished, so the framework default allows every origin. Confirmed on `c4eee46` across `200`, `403`, `404`, and `500`, and with a forged `Origin`.
- Decide whether Meilisearch index settings should be applied by code. Today `filterableAttributes` is set only by `php artisan scout:sync-index-settings`; no observer, job, or ingestion path applies it, so a fresh index answers `500` on faceted search until an operator runs the command. Either make ingestion ensure the settings or make the failure explicit instead of a generic `500`.
- Remove the duplicated security headers on API responses, sent by both nginx and the `SecurityHeaders` middleware.
- 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
48 changes: 48 additions & 0 deletions brain/handoffs/2026-09-23-production-smoke-csp-recheck.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Handoff - Production Smoke Re-Run With CSP Enforced

Date: 2026-09-23
Branch: `docs/c7-smoke-recheck` (validation only, no product change)
Stack built from: `c4eee46`

## Scope

The first production hydrated-browser smoke ran on `29a7881`. Content-Security-Policy (ADR-30), the optional bearer resolver (ADR-31), and correlation-ID telemetry (ADR-29) landed afterwards and changed `frontend/src/server.ts` and the backend middleware stack, so the proof no longer covered the shipped code. This round re-ran the smoke on current `main` and added CSP to what has to be proven.

## How It Was Validated

Two independent passes, each with its own public values, in throwaway Compose projects with their own volumes. Ports were published on the chosen IP only, so the default origin answered connection refused.

- Execution pass: public origin `127.0.0.212:4614`, API `:8514`, Reverb `:8714`.
- Audit pass: public origin `127.0.0.77:4677`, API `:8077`, Reverb `:8177`, built independently without the execution pass's method.

Port remapping used a Compose overlay in the scratchpad with the `!override` tag, because the default Compose merge appends to `ports` and would leave the defaults bound on `0.0.0.0`. No versioned file was touched; `git status` and `git diff HEAD` stayed empty.

## Proven

- Canonical, `og:url`, `og:image`, and JSON-LD carry the configured public origin on the landing, plugin, and command pages, with zero occurrences of a default origin. Prerendered routes go through the same renderer and carry CSP, nonce, and `Cache-Control: no-store`.
- `/runtime-config.js` serves the configured API origin, public origin, and Reverb block.
- After a proven hydration barrier, every captured request landed on a configured origin. Hydration was proven by a typed search that produced a result in the DOM and a `routerLink` navigation that issued no new document request.
- CSP is derived from `resolveRuntimeBrowserConfig()`, the same source as `/runtime-config.js`: `connect-src` carries the configured API and Reverb origins and no defaults. The `style-src` nonce changes on every response to the same URL.
- CSP is enforced rather than announced. Probes on a served page recorded `disposition: enforce` for an inline style without nonce, an inline script, a foreign script, and a foreign `fetch`, while a style carrying the header nonce applied normally.
- The browser websocket reaches the configured Reverb host under CSP and receives `pusher:connection_established`, with no violation.
- A forged `Host`, and a forged `X-Forwarded-Host` with `X-Forwarded-Proto: https`, never reach canonical or metadata.
- `POST /api/v1/auth/dev-login` answers `403 auth.dev_login_disabled` before request validation. The API sends `default-src 'none'` on success and error responses alike.

## Correction To The Record

The execution pass hit `500` with `Attribute community is not filterable` and attributed it to direct SQL inserts bypassing the Scout observers. The audit disproved that mechanism: building the dataset entirely through Eloquent, with observers and Horizon active, still produced an index with empty `filterableAttributes` and the same `500`.

The real fact is that no code path applies index settings. Only `php artisan scout:sync-index-settings` does, and it is the documented step in `README.md`. After it runs once, the normal path stays consistent, including a command created afterwards and searched with a community facet. This is an operational step, not a regression, and it is now recorded in `CURRENT_STATE.md` and as a backlog decision.

## Remaining Work

- Seeding stays broken in the production image, Priority 13, symptom reconfirmed on `c4eee46`.
- `Access-Control-Allow-Origin: *` confirmed on every API response, including errors and a forged `Origin`.
- API responses carry duplicated security headers from nginx and the `SecurityHeaders` middleware.
- Whether index settings should be applied by code, or the missing-settings failure surfaced explicitly instead of a generic `500`.

## Gates

- Production Docker runtime gate: `VALIDATED` in both passes, including migrations and Scout index sync.
- `db:seed` in the production image: `FAILED`, for the known Priority 13 reason.
- Backend and frontend unit, lint, and build gates: `PENDING`. This item changed no code, so they were not run.
Loading