diff --git a/docs/00_overview/BACKLOG_DASHBOARD.md b/docs/00_overview/BACKLOG_DASHBOARD.md index 2550ab4c..55839f19 100644 --- a/docs/00_overview/BACKLOG_DASHBOARD.md +++ b/docs/00_overview/BACKLOG_DASHBOARD.md @@ -2,7 +2,7 @@ # RelyLoop BACKLOG Dashboard -_Reflects feature-folder state as of **2026-06-06** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`backlog_dashboard.html`](backlog_dashboard.html) in a browser._ +_Reflects feature-folder state as of **2026-06-09** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`backlog_dashboard.html`](backlog_dashboard.html) in a browser._ ## Next up diff --git a/docs/00_overview/DASHBOARD.md b/docs/00_overview/DASHBOARD.md index b12a7246..d0de5b6b 100644 --- a/docs/00_overview/DASHBOARD.md +++ b/docs/00_overview/DASHBOARD.md @@ -1,15 +1,15 @@ # RelyLoop — Release Roadmap -_Top-level index across MVP1 → GA v1+ as of **2026-06-06**. Click a release name to drill into the per-release dashboard. Theme labels sourced from [`docs/01_architecture/tech-stack.md` §"Canonical release matrix"](../01_architecture/tech-stack.md). For the rich local view, open [`dashboard.html`](dashboard.html) in a browser._ +_Top-level index across MVP1 → GA v1+ as of **2026-06-09**. Click a release name to drill into the per-release dashboard. Theme labels sourced from [`docs/01_architecture/tech-stack.md` §"Canonical release matrix"](../01_architecture/tech-stack.md). For the rich local view, open [`dashboard.html`](dashboard.html) in a browser._ ## Releases | Release | Theme | Progress | Status | |---|---|---|---| | [MVP1 / v0.1](MVP1_DASHBOARD.md) | The Loop | 100 / 100 scoped done | **Complete** | -| [MVP2 / v0.2](MVP2_DASHBOARD.md) | Three-Engine + Real Signals | 25 / 28 scoped done · 13 remaining | **In progress** | +| [MVP2 / v0.2](MVP2_DASHBOARD.md) | Three-Engine + Real Signals | 25 / 28 scoped done · 17 remaining | **In progress** | | MVP3 / v0.3 | Observable | — | **Not yet scoped** | -| GA v1 / v1.0 | Production-ready | — | **Not yet scoped** | +| [GA v1 / v1.0](GA_DASHBOARD.md) | Production-ready | 1 item(s) queued | **Held / queued** | --- diff --git a/docs/00_overview/GA_DASHBOARD.md b/docs/00_overview/GA_DASHBOARD.md index 79a66d9b..9e4aaeb2 100644 --- a/docs/00_overview/GA_DASHBOARD.md +++ b/docs/00_overview/GA_DASHBOARD.md @@ -2,7 +2,7 @@ # RelyLoop GA Dashboard -_Reflects feature-folder state as of **2026-05-30** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`ga_dashboard.html`](ga_dashboard.html) in a browser._ +_Reflects feature-folder state as of **2026-06-09** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`ga_dashboard.html`](ga_dashboard.html) in a browser._ ## Next up @@ -14,12 +14,13 @@ Pull from the Idea backlog or capture a new feature spec. | Metric | Value | |---|---| -| Scoped items done | **0 / 0** (0%) — feat_/infra_/chore_/epic_ past idea stage | +| Filed under GA | **1** folders total (done + specced not-done + idea backlog + bugs) | +| Specced features done | **0 / 0** (0%) — of features *past the idea stage* (those with a spec); the idea backlog below is NOT in this denominator, so 100% ≠ release complete | | Pending work | **1** items (every not-done feat/infra/chore/bug across all priorities) | | → P0 — do next | **0** unblocking / paying daily cost | -| → P1 | **1** high-value, ready when P0 clears | +| → P1 | **0** high-value, ready when P0 clears | | → P2 (default) | 0 important to file, not blocking | -| → Backlog | 0 captured for record, not planned | +| → Backlog | 1 captured for record, not planned | | Open bugs | 0 | | Legacy "Path to GA" | 1 items — scoped-not-done + bugs + chore-ideas only (excludes feat/infra ideas) | | Backlog ideas | 0 idea-only feat/infra (not yet scoped into GA) | @@ -47,7 +48,7 @@ _None._ | # | Priority | Feature | Type | One-liner | Depends on | Status | |---|---|---|---|---|---|---| -| 1 | P1 | [chore_oss_public_launch_punchlist](planned_features/04_ga/chore_oss_public_launch_punchlist/idea.md) | Chore | The `chore_oss_launch_prep` PR adds the foundational governance / security / contributor files that prospective contributors and enterprise reviewers look for first. Three remaining items are gates on | — | Idea — captured during `chore_oss_launch_prep` (the PR that added SECURITY.md / GOVERNANCE.md / MAINTAINERS.md / CODEOWNERS / issue + PR templates and replaced the Code of Conduct) | +| 1 | Backlog | [chore_cors_credentials_origin_hardening](planned_features/04_ga/chore_cors_credentials_origin_hardening/idea.md) | Chore | CORS is configured with `allow_credentials=True` and an operator-configurable origin list: | — | Idea — surfaced during a codebase-wide security review (branch `claude/codebase-security-review-6njwio`) | ## Dependency graph diff --git a/docs/00_overview/MVP1_DASHBOARD.md b/docs/00_overview/MVP1_DASHBOARD.md index d78bd91f..c2c72f2b 100644 --- a/docs/00_overview/MVP1_DASHBOARD.md +++ b/docs/00_overview/MVP1_DASHBOARD.md @@ -2,7 +2,7 @@ # RelyLoop MVP1 Dashboard -_Reflects feature-folder state as of **2026-06-06** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`mvp1_dashboard.html`](mvp1_dashboard.html) in a browser._ +_Reflects feature-folder state as of **2026-06-09** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`mvp1_dashboard.html`](mvp1_dashboard.html) in a browser._ ## Next up diff --git a/docs/00_overview/MVP2_DASHBOARD.md b/docs/00_overview/MVP2_DASHBOARD.md index 66cc90c0..9b6ce04f 100644 --- a/docs/00_overview/MVP2_DASHBOARD.md +++ b/docs/00_overview/MVP2_DASHBOARD.md @@ -2,7 +2,7 @@ # RelyLoop MVP2 Dashboard -_Reflects feature-folder state as of **2026-06-06** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`mvp2_dashboard.html`](mvp2_dashboard.html) in a browser._ +_Reflects feature-folder state as of **2026-06-09** (latest mtime of any planned/implemented feature `.md` file). Regenerated by `make dashboard` and the `mvp1-dashboard-regen` pre-commit hook. For the rich local view (filter chips, type colors), open [`mvp2_dashboard.html`](mvp2_dashboard.html) in a browser._ ## Next up @@ -20,15 +20,15 @@ Plan approved; run /impl-execute to ship | Metric | Value | |---|---| -| Filed under MVP2 | **43** folders total (done + specced not-done + idea backlog + bugs) | +| Filed under MVP2 | **47** folders total (done + specced not-done + idea backlog + bugs) | | Specced features done | **25 / 28** (89%) — of features *past the idea stage* (those with a spec); the idea backlog below is NOT in this denominator, so 100% ≠ release complete | -| Pending work | **16** items (every not-done feat/infra/chore/bug across all priorities) | +| Pending work | **20** items (every not-done feat/infra/chore/bug across all priorities) | | → P0 — do next | **0** unblocking / paying daily cost | | → P1 | **0** high-value, ready when P0 clears | -| → P2 (default) | 9 important to file, not blocking | +| → P2 (default) | 13 important to file, not blocking | | → Backlog | 7 captured for record, not planned | -| Open bugs | 5 | -| Legacy "Path to MVP2" | 13 items — scoped-not-done + bugs + chore-ideas only (excludes feat/infra ideas) | +| Open bugs | 7 | +| Legacy "Path to MVP2" | 17 items — scoped-not-done + bugs + chore-ideas only (excludes feat/infra ideas) | | Backlog ideas | 3 idea-only feat/infra (not yet scoped into MVP2) | | In flight | 0 feature(s) actively shipping | @@ -82,23 +82,27 @@ _None._ _None._ -### Idea (13) +### Idea (17) | # | Priority | Feature | Type | One-liner | Depends on | Status | |---|---|---|---|---|---|---| -| 1 | P2 | [chore_overnight_result_card_screenshot](planned_features/02_mvp2/chore_overnight_result_card_screenshot/idea.md) | Chore | The `docs/08_guides/tutorial-first-study.md` Step 12 sub-section *"In the morning — read the overnight result card"* (verified at [tutorial-first-study.md:510](../docs/08_guides/tutorial-first-study.m | — | Idea — deferred FR-9 deliverable from PR #442 | -| 2 | P2 | [chore_solr_post_pipeline_followups](planned_features/02_mvp2/chore_solr_post_pipeline_followups/idea.md) | Chore | The 13-story `infra_adapter_solr` execution surfaced several follow-on items that fit neither the original spec nor any sister feature folder. None block the MVP2 Solr release — they're operator-exper | — | Idea — tangential observations from `infra_adapter_solr` end-to-end | -| 3 | P2 | [bug_e2e_teardown_chain_node_delete_500](planned_features/02_mvp2/bug_e2e_teardown_chain_node_delete_500/idea.md) | Bug | The E2E global-teardown deletes seeded rows in a fixed order (per `chore_e2e_test_rows_isolation` Story 1.2 cleanup registration). For auto-followup **chains**, the seeded nodes are `queued` studies c | — | Idea — tangential discovery during `feat_overnight_autopilot` (Story 4.2 E2E, PR forthcoming) | -| 4 | P2 | [bug_reseed_failure_blocks_retry_arq_singleton_dedup](planned_features/02_mvp2/bug_reseed_failure_blocks_retry_arq_singleton_dedup/idea.md) | Bug | `run_demo_reseed` is enqueued with a fixed Arq job id `demo_reseed:singleton` (the singleton concurrency guard). When a run reaches a terminal state, Arq stores its **result** under `arq:result:demo_r | — | Idea — tangential discovery while verifying `fix(demo): add Solr (8983) to the reseed engine host-URL mapping` (branch `feat_demo_reseed_solr_and_steplog`) | -| 5 | P2 | [bug_studies_detail_vitest_intermittent_timeout](planned_features/02_mvp2/bug_studies_detail_vitest_intermittent_timeout/idea.md) | Bug | Under the full `pnpm test` run (`vitest run`, default worker pool), the Study-detail-page render test sometimes blocks past the 5 s `testTimeout` default — but the test itself is data-driven from mock | — | Idea — captured during `chore_template_library_expansion` post-impl tangential sweep | -| 6 | P2 | [bug_webhook_concurrent_merge_race_timing_sensitive](planned_features/02_mvp2/bug_webhook_concurrent_merge_race_timing_sensitive/idea.md) | Bug | Idea — surfaced during `bug_demo_clusters_unreachable_in_healthz` PR #236 CI. | — | Idea — surfaced during `bug_demo_clusters_unreachable_in_healthz` PR #236 CI. | -| 7 | Backlog | [infra_arq_subprocess_test](planned_features/02_mvp2/infra_arq_subprocess_test/idea.md) | Infra | Idea (deferred from `feat_study_lifecycle` Phase 2 / PR #25 final GPT-5.5 review). Still applicable as of 2026-05-14: the three in-process tests cited below still cover the resume contract correctly; | — | Idea (deferred from `feat_study_lifecycle` Phase 2 / PR #25 final GPT-5.5 review). Still applicable as of 2026-05-14: the three in-process tests cited below still cover the resume contract correctly; a subprocess test would add a narrow Arq-version-regression guard. | -| 8 | Backlog | [infra_pr_yml_split_backend_test_lanes](planned_features/02_mvp2/infra_pr_yml_split_backend_test_lanes/idea.md) | Infra | The heavy `backend (tests + coverage)` job in `.github/workflows/pr.yml` runs the full `pytest backend/tests/` matrix (unit + integration + contract) serially in one job with `--cov` gating at `fail_u | — | Idea — **deferred (defer-until-binding-constraint)**. Carved out of `chore_pr_yml_parallelize_backend_job` (now in `implemented_features/2026_06_05_*`; see "Relationship to other work" below for the link) at its 2026-06-05 descope. Pick up only when the integration layer becomes the binding CI constraint after other critical-path work lands. | -| 9 | Backlog | [infra_smoke_fork_pr_secret_skip](planned_features/02_mvp2/infra_smoke_fork_pr_secret_skip/idea.md) | Infra | `.github/workflows/pr.yml` triggers on `pull_request:` ([pr.yml:43](../.github/workflows/pr.yml)) — **not** `pull_request_target`. GitHub deliberately withholds repository secrets from workflows trigg | — | Idea — tangential discovery while merging PR #387 (`chore_arq_pool_aclose_deprecation`) | -| 10 | Backlog | [chore_auto_followup_parent_advisory_lock](planned_features/02_mvp2/chore_auto_followup_parent_advisory_lock/idea.md) | Chore | The shipped `feat_auto_followup_studies` worker uses a two-layer idempotency scheme: | — | Idea — captured as a standalone file to resolve broken cross-references in `feat_auto_followup_studies` D-11 + plan F2 + `bug_auto_followup_completed_parent_stop_chain_race/idea.md`. The slug was coined 2026-05-24 in D-11 but only existed as descriptive prose across other documents until now. | -| 11 | Backlog | [chore_e2e_overnight_strategy_radix_select_timing](planned_features/02_mvp2/chore_e2e_overnight_strategy_radix_select_timing/idea.md) | Chore | The Story 3.2 E2E spec walks the create-study wizard to Step 5, clicks the depth `` becomes visible. In chromium against `pnpm dev`, t | — | Idea — tangential follow-up captured during `feat_overnight_final_solution` Story 3.2 implementation | -| 12 | Backlog | [chore_ubi_hybrid_template_render](planned_features/02_mvp2/chore_ubi_hybrid_template_render/idea.md) | Chore | Idea — contract decision deferred (NOT a worker bug) | — | Idea — contract decision deferred (NOT a worker bug) | -| 13 | Backlog | [bug_chat_long_conversation_truncation](planned_features/02_mvp2/bug_chat_long_conversation_truncation/idea.md) | Bug | [`backend/app/services/agent_chat.send_user_message`](../../backend/app/services/agent_chat.py) defensively caps the OpenAI history at the most recent `HISTORY_MAX_MESSAGES = 100` messages… | — | Held for MVP2 (decided 2026-05-13). Folder renamed with `_mvp2` suffix to make the deferral visible at-a-glance in `ls docs/00_overview/planned_features/`. Resume work when MVP2 starts — no technical dependency on MVP2 infra (audit_log is N/A; Langfuse is convenience only); the deferral is scope discipline + zero current impact (latent bug, no operator has hit the 100-message cap). | +| 1 | P2 | [chore_agent_confirmation_tool_name_word_boundary](planned_features/02_mvp2/chore_agent_confirmation_tool_name_word_boundary/idea.md) | Chore | The confirmation gate for the 8 mutating agent tools (`create_study`, `cancel_study`, `open_pr`, `create_proposal_*`, judgment generation, CSV import) is a two-condition heuristic: the last assistant | — | Idea — surfaced during a codebase-wide security review (branch `claude/codebase-security-review-6njwio`) | +| 2 | P2 | [chore_overnight_result_card_screenshot](planned_features/02_mvp2/chore_overnight_result_card_screenshot/idea.md) | Chore | The `docs/08_guides/tutorial-first-study.md` Step 12 sub-section *"In the morning — read the overnight result card"* (verified at [tutorial-first-study.md:510](../docs/08_guides/tutorial-first-study.m | — | Idea — deferred FR-9 deliverable from PR #442 | +| 3 | P2 | [chore_solr_post_pipeline_followups](planned_features/02_mvp2/chore_solr_post_pipeline_followups/idea.md) | Chore | The 13-story `infra_adapter_solr` execution surfaced several follow-on items that fit neither the original spec nor any sister feature folder. None block the MVP2 Solr release — they're operator-exper | — | Idea — tangential observations from `infra_adapter_solr` end-to-end | +| 4 | P2 | [chore_test_router_conditional_mount](planned_features/02_mvp2/chore_test_router_conditional_mount/idea.md) | Chore | The `_test` router exposes data-mutating endpoints used only for deterministic E2E (seed a completed study, demo reseed, hard-delete studies/judgment-lists/proposals). Today it is registered **uncondi | — | Idea — surfaced during a codebase-wide security review (branch `claude/codebase-security-review-6njwio`) | +| 5 | P2 | [bug_cluster_url_ssrf_hostname_bypass](planned_features/02_mvp2/bug_cluster_url_ssrf_hostname_bypass/idea.md) | Bug | The cluster registration `base_url` validator is intended to stop SSRF into internal/cloud-metadata endpoints (it cites "spec §10 Threat 3"), but the guard only fires when the host parses as a **liter | — | Idea — surfaced during a codebase-wide security review (branch `claude/codebase-security-review-6njwio`) | +| 6 | P2 | [bug_e2e_teardown_chain_node_delete_500](planned_features/02_mvp2/bug_e2e_teardown_chain_node_delete_500/idea.md) | Bug | The E2E global-teardown deletes seeded rows in a fixed order (per `chore_e2e_test_rows_isolation` Story 1.2 cleanup registration). For auto-followup **chains**, the seeded nodes are `queued` studies c | — | Idea — tangential discovery during `feat_overnight_autopilot` (Story 4.2 E2E, PR forthcoming) | +| 7 | P2 | [bug_request_id_header_unvalidated_log_injection](planned_features/02_mvp2/bug_request_id_header_unvalidated_log_injection/idea.md) | Bug | `RequestIDMiddleware` adopts a client-supplied `X-Request-ID` header verbatim with no validation of length or character set: | — | Idea — surfaced during a codebase-wide security review (branch `claude/codebase-security-review-6njwio`) | +| 8 | P2 | [bug_reseed_failure_blocks_retry_arq_singleton_dedup](planned_features/02_mvp2/bug_reseed_failure_blocks_retry_arq_singleton_dedup/idea.md) | Bug | `run_demo_reseed` is enqueued with a fixed Arq job id `demo_reseed:singleton` (the singleton concurrency guard). When a run reaches a terminal state, Arq stores its **result** under `arq:result:demo_r | — | Idea — tangential discovery while verifying `fix(demo): add Solr (8983) to the reseed engine host-URL mapping` (branch `feat_demo_reseed_solr_and_steplog`) | +| 9 | P2 | [bug_studies_detail_vitest_intermittent_timeout](planned_features/02_mvp2/bug_studies_detail_vitest_intermittent_timeout/idea.md) | Bug | Under the full `pnpm test` run (`vitest run`, default worker pool), the Study-detail-page render test sometimes blocks past the 5 s `testTimeout` default — but the test itself is data-driven from mock | — | Idea — captured during `chore_template_library_expansion` post-impl tangential sweep | +| 10 | P2 | [bug_webhook_concurrent_merge_race_timing_sensitive](planned_features/02_mvp2/bug_webhook_concurrent_merge_race_timing_sensitive/idea.md) | Bug | Idea — surfaced during `bug_demo_clusters_unreachable_in_healthz` PR #236 CI. | — | Idea — surfaced during `bug_demo_clusters_unreachable_in_healthz` PR #236 CI. | +| 11 | Backlog | [infra_arq_subprocess_test](planned_features/02_mvp2/infra_arq_subprocess_test/idea.md) | Infra | Idea (deferred from `feat_study_lifecycle` Phase 2 / PR #25 final GPT-5.5 review). Still applicable as of 2026-05-14: the three in-process tests cited below still cover the resume contract correctly; | — | Idea (deferred from `feat_study_lifecycle` Phase 2 / PR #25 final GPT-5.5 review). Still applicable as of 2026-05-14: the three in-process tests cited below still cover the resume contract correctly; a subprocess test would add a narrow Arq-version-regression guard. | +| 12 | Backlog | [infra_pr_yml_split_backend_test_lanes](planned_features/02_mvp2/infra_pr_yml_split_backend_test_lanes/idea.md) | Infra | The heavy `backend (tests + coverage)` job in `.github/workflows/pr.yml` runs the full `pytest backend/tests/` matrix (unit + integration + contract) serially in one job with `--cov` gating at `fail_u | — | Idea — **deferred (defer-until-binding-constraint)**. Carved out of `chore_pr_yml_parallelize_backend_job` (now in `implemented_features/2026_06_05_*`; see "Relationship to other work" below for the link) at its 2026-06-05 descope. Pick up only when the integration layer becomes the binding CI constraint after other critical-path work lands. | +| 13 | Backlog | [infra_smoke_fork_pr_secret_skip](planned_features/02_mvp2/infra_smoke_fork_pr_secret_skip/idea.md) | Infra | `.github/workflows/pr.yml` triggers on `pull_request:` ([pr.yml:43](../.github/workflows/pr.yml)) — **not** `pull_request_target`. GitHub deliberately withholds repository secrets from workflows trigg | — | Idea — tangential discovery while merging PR #387 (`chore_arq_pool_aclose_deprecation`) | +| 14 | Backlog | [chore_auto_followup_parent_advisory_lock](planned_features/02_mvp2/chore_auto_followup_parent_advisory_lock/idea.md) | Chore | The shipped `feat_auto_followup_studies` worker uses a two-layer idempotency scheme: | — | Idea — captured as a standalone file to resolve broken cross-references in `feat_auto_followup_studies` D-11 + plan F2 + `bug_auto_followup_completed_parent_stop_chain_race/idea.md`. The slug was coined 2026-05-24 in D-11 but only existed as descriptive prose across other documents until now. | +| 15 | Backlog | [chore_e2e_overnight_strategy_radix_select_timing](planned_features/02_mvp2/chore_e2e_overnight_strategy_radix_select_timing/idea.md) | Chore | The Story 3.2 E2E spec walks the create-study wizard to Step 5, clicks the depth `` becomes visible. In chromium against `pnpm dev`, t | — | Idea — tangential follow-up captured during `feat_overnight_final_solution` Story 3.2 implementation | +| 16 | Backlog | [chore_ubi_hybrid_template_render](planned_features/02_mvp2/chore_ubi_hybrid_template_render/idea.md) | Chore | Idea — contract decision deferred (NOT a worker bug) | — | Idea — contract decision deferred (NOT a worker bug) | +| 17 | Backlog | [bug_chat_long_conversation_truncation](planned_features/02_mvp2/bug_chat_long_conversation_truncation/idea.md) | Bug | [`backend/app/services/agent_chat.send_user_message`](../../backend/app/services/agent_chat.py) defensively caps the OpenAI history at the most recent `HISTORY_MAX_MESSAGES = 100` messages… | — | Held for MVP2 (decided 2026-05-13). Folder renamed with `_mvp2` suffix to make the deferral visible at-a-glance in `ls docs/00_overview/planned_features/`. Resume work when MVP2 starts — no technical dependency on MVP2 infra (audit_log is N/A; Langfuse is convenience only); the deferral is scope discipline + zero current impact (latent bug, no operator has hit the 100-message cap). | ## Dependency graph diff --git a/docs/00_overview/backlog_dashboard.html b/docs/00_overview/backlog_dashboard.html index c836f12d..e9593916 100644 --- a/docs/00_overview/backlog_dashboard.html +++ b/docs/00_overview/backlog_dashboard.html @@ -369,7 +369,7 @@

RelyLoop BACKLOG Dashboard

- Reflects feature-folder state as of 2026-06-06 (latest mtime of any + Reflects feature-folder state as of 2026-06-09 (latest mtime of any docs/00_overview/planned_features/ or docs/00_overview/implemented_features/ file). See state.md for the active branch context, diff --git a/docs/00_overview/dashboard.html b/docs/00_overview/dashboard.html index ceab90ff..fa07221b 100644 --- a/docs/00_overview/dashboard.html +++ b/docs/00_overview/dashboard.html @@ -368,7 +368,7 @@

RelyLoop — Release Roadmap

- Top-level index across MVP1 → GA v1+ as of 2026-06-06. Click a release name to + Top-level index across MVP1 → GA v1+ as of 2026-06-09. Click a release name to drill into the per-release dashboard. Theme labels sourced from tech-stack.md §"Canonical release matrix". See state.md for @@ -392,7 +392,7 @@

Releases

Three-Engine + Real Signals
-
25 / 28 scoped done · 13 remaining
+
25 / 28 scoped done · 17 remaining
In progress
@@ -406,10 +406,10 @@

Releases

-
GA v1 / v1.0
+
Production-ready
-
- Not yet scoped +
1 item(s) queued
+ Held / queued
diff --git a/docs/00_overview/ga_dashboard.html b/docs/00_overview/ga_dashboard.html index 6a883844..e61cd5cf 100644 --- a/docs/00_overview/ga_dashboard.html +++ b/docs/00_overview/ga_dashboard.html @@ -369,7 +369,7 @@

RelyLoop GA Dashboard

- Reflects feature-folder state as of 2026-05-30 (latest mtime of any + Reflects feature-folder state as of 2026-06-09 (latest mtime of any docs/00_overview/planned_features/ or docs/00_overview/implemented_features/ file). See state.md for the active branch context, @@ -396,9 +396,9 @@

RelyLoop GA Dashboard

GA Progress

-
Scoped items done
+
Specced features done
0 / 0
-
0% of feat_/infra_/chore_/epic_ items past idea stage
+
0% specced · 1 filed under GA
@@ -420,7 +420,7 @@

GA Progress

P1
-
1
+
0
high-value, ready when P0 clears
@@ -430,7 +430,7 @@

GA Progress

Backlog
-
0
+
1
captured for record, not planned
@@ -465,14 +465,14 @@

Pipeline

Idea 1

-
- +
+
Chore - P1 + Backlog
-
The `chore_oss_launch_prep` PR adds the foundational governance / security / contributor files that prospective contributors and enterprise reviewers look for first. Three remaining items are gates on
+
CORS is configured with `allow_credentials=True` and an operator-configurable origin list:
diff --git a/docs/00_overview/mvp1_dashboard.html b/docs/00_overview/mvp1_dashboard.html index d9b64acc..a45c34c7 100644 --- a/docs/00_overview/mvp1_dashboard.html +++ b/docs/00_overview/mvp1_dashboard.html @@ -369,7 +369,7 @@

RelyLoop MVP1 Dashboard

- Reflects feature-folder state as of 2026-06-06 (latest mtime of any + Reflects feature-folder state as of 2026-06-09 (latest mtime of any docs/00_overview/planned_features/ or docs/00_overview/implemented_features/ file). See state.md for the active branch context, diff --git a/docs/00_overview/mvp2_dashboard.html b/docs/00_overview/mvp2_dashboard.html index f9b1312c..7b3e2887 100644 --- a/docs/00_overview/mvp2_dashboard.html +++ b/docs/00_overview/mvp2_dashboard.html @@ -369,7 +369,7 @@

RelyLoop MVP2 Dashboard

- Reflects feature-folder state as of 2026-06-06 (latest mtime of any + Reflects feature-folder state as of 2026-06-09 (latest mtime of any docs/00_overview/planned_features/ or docs/00_overview/implemented_features/ file). See state.md for the active branch context, @@ -398,17 +398,17 @@

MVP2 Progress

Specced features done
25 / 28
-
89% specced · 43 filed under MVP2
+
89% specced · 47 filed under MVP2
Pending work
-
16
+
20
every not-done feat/infra/chore/bug across all priorities
Open bugs
-
5
+
7
tracked bug_* idea files
@@ -425,7 +425,7 @@

MVP2 Progress

P2 (default)
-
9
+
13
important to file, not blocking
@@ -435,7 +435,7 @@

MVP2 Progress

Legacy "Path to MVP2"
-
13
+
17
scoped not-done + bugs + chore-ideas only (excludes feat/infra ideas)
@@ -463,7 +463,20 @@

Pipeline

-

Idea 13

+

Idea 17

+ +
+ +
+ Chore + P2 + +
+
The confirmation gate for the 8 mutating agent tools (`create_study`, `cancel_study`, `open_pr`, `create_proposal_*`, judgment generation, CSV import) is a two-condition heuristic: the last assistant
+ + +
+
@@ -491,6 +504,32 @@

Idea 13

+
+ +
+ Chore + P2 + +
+
The `_test` router exposes data-mutating endpoints used only for deterministic E2E (seed a completed study, demo reseed, hard-delete studies/judgment-lists/proposals). Today it is registered **uncondi
+ + +
+ + +
+ +
+ Bug + P2 + +
+
The cluster registration `base_url` validator is intended to stop SSRF into internal/cloud-metadata endpoints (it cites "spec §10 Threat 3"), but the guard only fires when the host parses as a **liter
+ + +
+ +
@@ -504,6 +543,19 @@

Idea 13

+
+ +
+ Bug + P2 + +
+
`RequestIDMiddleware` adopts a client-supplied `X-Request-ID` header verbatim with no validation of length or character set:
+ + +
+ +
diff --git a/docs/00_overview/planned_features/02_mvp2/bug_cluster_url_ssrf_hostname_bypass/idea.md b/docs/00_overview/planned_features/02_mvp2/bug_cluster_url_ssrf_hostname_bypass/idea.md new file mode 100644 index 00000000..02208ab2 --- /dev/null +++ b/docs/00_overview/planned_features/02_mvp2/bug_cluster_url_ssrf_hostname_bypass/idea.md @@ -0,0 +1,57 @@ +# bug_cluster_url_ssrf_hostname_bypass — cluster base_url SSRF guard only inspects literal IPs + +**Date:** 2026-06-09 +**Status:** Idea — surfaced during a codebase-wide security review (branch `claude/codebase-security-review-6njwio`) +**Priority:** P2 +**Origin:** Security review of `backend/app/adapters/` + cluster registration; finding in `backend/app/api/v1/schemas.py` +**Depends on:** None + +## Problem + +The cluster registration `base_url` validator is intended to stop SSRF into internal/cloud-metadata endpoints (it cites "spec §10 Threat 3"), but the guard only fires when the host parses as a **literal IP address**. Any DNS **hostname** is returned unchecked, so the protection is trivially bypassable and gives a false sense of safety. + +`backend/app/api/v1/schemas.py:128-143` (`CreateClusterRequest.validate_base_url`) and the duplicated logic at `schemas.py:162-180` (`ConnectionTestRequest.validate_base_url`): + +```python +try: + ip = ip_address(parsed.hostname) +except ValueError: + return v # hostname; skip private-IP check <-- line 137 / 174 +if (ip.is_private or ip.is_loopback) and not get_settings().relyloop_allow_private_clusters: + raise ValueError(...) +``` + +Consequences: +1. **Hostname bypass (primary).** `http://metadata.google.internal/computeMetadata/v1/...` (GCP's metadata endpoint, reachable by name), `http://localhost.localdomain`, or any internal service DNS name (`http://vault.internal:8200`) passes validation unconditionally. The adapter then issues HTTP requests to it during the registration probe / `test-connection` and on every health probe, and the response body is cached in Redis and surfaced via cluster detail + `/healthz`. That is a read-SSRF exfiltration path. +2. **DNS rebinding.** Even if a literal-IP check passed, validation-time and connect-time are different moments; an attacker-controlled name can resolve to a benign IP at validation and an internal IP at probe time. The docstring explicitly says "DNS resolution is intentionally NOT performed at validation time," so this is a known-but-undefended gap. +3. **Carrier-grade NAT range** `100.64.0.0/10` reports `is_private=False`, so it is not blocked even as a literal IP (minor). + +Note on what is **already** covered (so the fix is scoped accurately): on Python 3.11.4+/3.12.4+/3.13, `ipaddress.ip_address("169.254.169.254").is_private` is `True`, so the **AWS link-local metadata IP is already blocked** when `RELYLOOP_ALLOW_PRIVATE_CLUSTERS` is false (the MVP1 default). IPv6 link-local (`fe80::/10`) and `0.0.0.0` are likewise `is_private=True`. The gap is the hostname path, not the literal-IP path. + +Severity in the MVP1 posture is **Medium**: single-tenant, no auth, local-only — anyone who can reach the API can already register a cluster pointing anywhere, so this is not a privilege-escalation across a trust boundary today. It becomes **High** the moment RelyLoop is deployed on a cloud host (IAM-role metadata reachable) or behind any auth boundary, which is exactly where the validator is supposed to earn its keep. + +## Proposed capabilities + +### Close the hostname SSRF path + +- Resolve the hostname and apply the same `is_private / is_loopback / is_link_local / is_reserved / is_multicast / is_unspecified` rejection to **every** resolved address, not just literal-IP hosts. Reject if any resolved A/AAAA record lands in a blocked range while `RELYLOOP_ALLOW_PRIVATE_CLUSTERS` is false. **Do the DNS resolution asynchronously at the service/controller layer** (the cluster-registration + `test-connection` handlers), **not inside the Pydantic field validator** — Pydantic field validators are synchronous, so blocking DNS I/O there would stall the event loop. Keep the pure, no-I/O checks (scheme, literal-IP range classification) in the validator; move the resolve-and-check to the async handler. +- Add an explicit denylist for well-known metadata hostnames (`metadata.google.internal`, `metadata`, etc.) as defense-in-depth regardless of DNS resolution — this stays cheap enough to keep in the sync validator. +- Consolidate the duplicated literal-IP/scheme validator (it is copy-pasted across `CreateClusterRequest` and `ConnectionTestRequest`) into one shared helper so the two paths cannot drift, and share the async resolve-and-check helper between the two handlers. +- For the DNS-rebinding window: pin the resolved IP and re-validate it at the adapter connect call (or document the residual risk explicitly in `docs/04_security/` as an operator-network responsibility if resolve-and-pin is deferred). +- Add `is_link_local`/`is_reserved`/`is_multicast`/`is_unspecified`/`100.64.0.0/10` to the literal-IP rejection set for completeness. + +## Scope signals + +- **Backend:** `backend/app/api/v1/schemas.py` (two validators → one helper); possibly a connect-time re-check in `backend/app/adapters/elastic.py` + `solr.py`. New unit tests for hostname-resolving-to-private, metadata hostname, IPv6 link-local, CGNAT. +- **Frontend:** none. +- **Migration:** none. +- **Config:** reuses `RELYLOOP_ALLOW_PRIVATE_CLUSTERS`; possibly add a metadata-hostname denylist constant. +- **Audit events:** N/A. + +## Why filed as an idea rather than fixed inline + +Surfaced during a read-only security review, not while touching the cluster registration path. The fix has a real design fork (resolve-at-validation vs resolve-and-pin-at-connect vs denylist-only) with a latency/DNS-I/O tradeoff the spec called out as deliberately deferred — that is a design decision, not a one-line correction, so it warrants a spec rather than an unilateral inline edit. The link-local-IP half of the original review claim was already covered by modern `ipaddress` semantics; this idea narrows the finding to the genuine hostname gap. + +## Relationship to other work + +Touches the same validator the cluster-registration / `test-connection` features own. Independent of the other security-review idea files filed in the same review sweep (request-ID validation, agent-confirmation matching, CORS-credentials hardening, test-router conditional mount). diff --git a/docs/00_overview/planned_features/02_mvp2/bug_request_id_header_unvalidated_log_injection/idea.md b/docs/00_overview/planned_features/02_mvp2/bug_request_id_header_unvalidated_log_injection/idea.md new file mode 100644 index 00000000..aa672c01 --- /dev/null +++ b/docs/00_overview/planned_features/02_mvp2/bug_request_id_header_unvalidated_log_injection/idea.md @@ -0,0 +1,47 @@ +# bug_request_id_header_unvalidated_log_injection — client X-Request-ID adopted without validation + +**Date:** 2026-06-09 +**Status:** Idea — surfaced during a codebase-wide security review (branch `claude/codebase-security-review-6njwio`) +**Priority:** P2 +**Origin:** Security review; finding in `backend/app/api/middleware.py` +**Depends on:** None + +## Problem + +`RequestIDMiddleware` adopts a client-supplied `X-Request-ID` header verbatim with no validation of length or character set: + +`backend/app/api/middleware.py:50` +```python +request_id = request.headers.get(REQUEST_ID_HEADER) or str(uuid_utils.uuid7()) +``` + +The adopted value is then (a) bound to the structlog context for every log line emitted during the request (`middleware.py:56`) and (b) echoed back in the `X-Request-ID` response header (`middleware.py:66`). Because it is treated as fully opaque, a client can supply: + +- **Log-injection payloads** — CRLF / control characters that, once interpolated into a JSON or line-oriented log record consumed by a downstream aggregator, can forge or split log lines (anti-forensics, alert evasion). The structlog JSON renderer escapes within a string field, but any non-JSON sink or partial log scrape is exposed. +- **Unbounded length** — a multi-megabyte header value is copied into the contextvar and emitted on every log line for the request, inflating log volume/memory on a per-request basis (cheap, repeatable resource amplification). +- **Response-header reflection** — the value is written straight back into a response header; depending on the ASGI server's header validation this is a header-injection surface. + +This is **Low** severity in the MVP1 posture (single-tenant, local, no auth), but it is a gratuitous trust of unvalidated client input on the request-correlation path that every request flows through, and it is a one-function fix. + +## Proposed capabilities + +### Validate or re-mint the request ID + +- Accept a client `X-Request-ID` only if it matches a strict pattern (e.g. `^[A-Za-z0-9._-]{1,128}$`); otherwise mint a fresh UUIDv7 and ignore the client value. This preserves the documented idempotent-retry-correlation use case for well-behaved clients while removing the injection/amplification surface. +- Add unit tests: oversized value re-minted, CRLF value re-minted, valid value adopted, absent value minted. + +## Scope signals + +- **Backend:** `backend/app/api/middleware.py` (one function) + a unit test file. +- **Frontend:** none (the UI's `api-client.ts` already sends UUIDv7s, which match the pattern). +- **Migration:** none. +- **Config:** none. +- **Audit events:** N/A. + +## Why filed as an idea rather than fixed inline + +Genuinely borderline — this is close to the "≤50 LOC, no design fork" inline-fix threshold and could reasonably be fixed in a single small PR. It is captured here because it surfaced during a read-only review sweep alongside several sibling findings; bundle it into the same security-hardening batch rather than touching middleware out of band. If picked up first, it is a clean `/impl-execute --ad-hoc`. + +## Relationship to other work + +Part of the security-review idea sweep on branch `claude/codebase-security-review-6njwio` (siblings: cluster-URL SSRF, agent-confirmation matching, CORS-credentials, test-router mount). Independent of all of them. diff --git a/docs/00_overview/planned_features/02_mvp2/chore_agent_confirmation_tool_name_word_boundary/idea.md b/docs/00_overview/planned_features/02_mvp2/chore_agent_confirmation_tool_name_word_boundary/idea.md new file mode 100644 index 00000000..a94faf32 --- /dev/null +++ b/docs/00_overview/planned_features/02_mvp2/chore_agent_confirmation_tool_name_word_boundary/idea.md @@ -0,0 +1,51 @@ +# chore_agent_confirmation_tool_name_word_boundary — tighten the mutating-tool confirmation match + +**Date:** 2026-06-09 +**Status:** Idea — surfaced during a codebase-wide security review (branch `claude/codebase-security-review-6njwio`) +**Priority:** P2 +**Origin:** Security review of the chat agent; finding in `backend/app/agent/orchestrator.py` + `backend/app/agent/confirmation.py` +**Depends on:** None + +## Problem + +The confirmation gate for the 8 mutating agent tools (`create_study`, `cancel_study`, `open_pr`, `create_proposal_*`, judgment generation, CSV import) is a two-condition heuristic: the last assistant message must "mention the tool name" AND the last user message must read as affirmative. Two aspects of the match are looser than the gate's intent: + +`backend/app/agent/orchestrator.py:129-133` (`_is_authorized_mutation`) +```python +tool_name_spaced = tool_name.replace("_", " ") +assistant_lower = last_assistant_text.lower() +if tool_name not in assistant_lower and tool_name_spaced not in assistant_lower: + return False +return is_affirmative(last_user_text) +``` + +1. **Substring (not word-boundary) match.** `tool_name in assistant_lower` is a bare substring test, so adjacent text can satisfy it unintentionally. Word-boundary matching is the documented intent (`is_affirmative` already uses `\b`-style whole-word matching for exactly this reason). +2. **Any-mutating-tool-named authorizes that tool with one generic affirmative.** If a single assistant turn names more than one mutating tool ("I can create a study and then open a PR for you") and the user replies "yes", the gate passes for **every** named tool the model subsequently emits in that step — the affirmative is not bound to a specific proposed action. The gate cannot distinguish "yes to create_study" from "yes, do all of that." + +The prompt-injection-via-tool-result vector is **already defended** — `orchestrator.py` delimits tool/document content in the LLM history with an "ignore embedded instructions" note (spec §10 Threat 4), and the affirmative must come from the genuine user turn — so a malicious indexed document cannot itself supply the affirmation. That is why this is **Low/Medium**, not High. The `is_affirmative` helper is also already hardened against negation ("don't do it", "no go" → False, per `confirmation.py:64-99`). + +The function's own docstring frames the heuristic as MVP1-acceptable ("a strict state-machine confirmation can land at MVP2 if the heuristic misfires"), so this idea is the scheduled follow-up that docstring anticipates. + +## Proposed capabilities + +### Bind the confirmation to a specific proposed tool + +- Switch the tool-name test from substring to whole-word/boundary matching. Note the matcher must preserve underscores: the `re.findall(r"[a-z]+", ...)` tokenizer used by `is_affirmative` would split `create_study` into `["create", "study"]`, so a full-name membership test against those tokens always fails. Use a `\b`-anchored regex directly on the tool name (`re.search(rf"\b{re.escape(name)}\b", text)`) — accepting both the underscored (`create_study`) and spaced (`create study`) forms — or tokenize with `[a-z_]+`. +- Tighten the model: require the assistant to have proposed a **single** specific tool (or track which tool the affirmative answers) so one "yes" cannot blanket-authorize multiple mutating calls emitted in the same step. A lightweight option: only authorize a mutating tool if exactly one mutating tool name appears in the last assistant turn; otherwise require an explicit per-tool re-prompt. +- Add unit tests for: multi-tool turn + generic "yes" (must NOT authorize all), substring-collision negative case, single-tool happy path, negation path (already covered — keep green). + +## Scope signals + +- **Backend:** `backend/app/agent/orchestrator.py` (`_is_authorized_mutation`) + `backend/app/agent/confirmation.py`; unit tests in `backend/tests/unit/agent/`. +- **Frontend:** none. +- **Migration:** none. +- **Config:** none. +- **Audit events:** N/A (MVP2-pre). + +## Why filed as an idea rather than fixed inline + +The "bind one affirmative to one specific tool" change is a small **product/UX decision** about the agent's confirmation flow (how the assistant must phrase a multi-action proposal), not a mechanical correction — it warrants a spec so the conversational contract is decided deliberately. The pure word-boundary swap alone could be inline, but it is bundled here so the matching and the binding model are designed together rather than half-fixed. + +## Relationship to other work + +Part of the security-review idea sweep on branch `claude/codebase-security-review-6njwio`. Independent of the SSRF, request-ID, CORS, and test-router siblings. diff --git a/docs/00_overview/planned_features/02_mvp2/chore_test_router_conditional_mount/idea.md b/docs/00_overview/planned_features/02_mvp2/chore_test_router_conditional_mount/idea.md new file mode 100644 index 00000000..73300c43 --- /dev/null +++ b/docs/00_overview/planned_features/02_mvp2/chore_test_router_conditional_mount/idea.md @@ -0,0 +1,46 @@ +# chore_test_router_conditional_mount — defense-in-depth for the dev-only `_test` router + +**Date:** 2026-06-09 +**Status:** Idea — surfaced during a codebase-wide security review (branch `claude/codebase-security-review-6njwio`) +**Priority:** P2 +**Origin:** Security review of the FastAPI app surface; finding in `backend/app/main.py` + `backend/app/api/v1/_test.py` +**Depends on:** None + +## Problem + +The `_test` router exposes data-mutating endpoints used only for deterministic E2E (seed a completed study, demo reseed, hard-delete studies/judgment-lists/proposals). Today it is registered **unconditionally** at app boot: + +`backend/app/main.py:219-221` +```python +app.include_router( + test_router.router, prefix="/api/v1" +) # infra_e2e_seed_completed_study — dev-only; 404 outside +``` + +Containment currently rests entirely on a **per-endpoint** dependency: every route carries `dependencies=[Depends(_require_development_env)]`, which returns 404 whenever `Settings.environment != "development"` (verified — the guard is default-deny, so an accidental `ENVIRONMENT=production` or any unset/misconfigured value correctly disappears the surface). So this is **not** a live vulnerability — the existing gate is sound. + +The weakness is structural, not behavioral: the safety is one decorator argument per route, repeated by hand 9 times. The day someone adds a 10th endpoint to this router and forgets the `dependencies=[...]` line, it ships wide open in every environment, and nothing fails — there is no test asserting the invariant. For a router whose endpoints hard-delete rows and wipe/reseed demo data, that is a fragile place to rely on copy-paste discipline. + +## Proposed capabilities + +### Make "dev-only" structural, not per-route + +- **Primary:** attach `_require_development_env` as a **router-level** dependency (`APIRouter(dependencies=[Depends(_require_development_env)])`) so individual routes physically cannot opt out — this is the structural fix and needs no settings access at import time. +- **Optionally also** register the router conditionally at boot. If a module-load environment check is used, read the env directly (`os.environ.get("ENVIRONMENT") == "development"`) rather than building the full `get_settings()` object at import time, to avoid the import-time-settings caveat that bites unit tests imported without the runtime stack. (Note: `main.py`'s CORS block already reads `get_settings()` at module load, so this is a soft preference, not a hard rule — but the router-level dependency above makes the conditional mount unnecessary anyway.) Keep the per-endpoint guards too — belt-and-suspenders. +- Add a guard test that introspects `test_router.router.routes` and asserts every route carries the development-env dependency — so a future un-gated endpoint fails CI instead of shipping. + +## Scope signals + +- **Backend:** `backend/app/main.py` (conditional include) + `backend/app/api/v1/_test.py` (router-level dep) + a contract/unit guard test. +- **Frontend:** none. +- **Migration:** none. +- **Config:** none (reuses `ENVIRONMENT`). +- **Audit events:** N/A. + +## Why filed as an idea rather than fixed inline + +Small enough to be inline, but captured during a read-only review sweep with no behavior change today (the surface is already correctly 404'd outside dev). It is a defense-in-depth + regression-guard task whose value is the test asserting the invariant; bundle it into the security-hardening batch. Clean `/impl-execute --ad-hoc` if picked up alone. + +## Relationship to other work + +Part of the security-review idea sweep on branch `claude/codebase-security-review-6njwio`. Independent of the SSRF, request-ID, agent-confirmation, and CORS siblings. diff --git a/docs/00_overview/planned_features/04_ga/chore_cors_credentials_origin_hardening/idea.md b/docs/00_overview/planned_features/04_ga/chore_cors_credentials_origin_hardening/idea.md new file mode 100644 index 00000000..0a634083 --- /dev/null +++ b/docs/00_overview/planned_features/04_ga/chore_cors_credentials_origin_hardening/idea.md @@ -0,0 +1,51 @@ +# chore_cors_credentials_origin_hardening — guard allow_credentials against over-broad origins + +**Date:** 2026-06-09 +**Status:** Idea — surfaced during a codebase-wide security review (branch `claude/codebase-security-review-6njwio`) +**Priority:** Backlog +**Origin:** Security review of the FastAPI app surface; finding in `backend/app/main.py` +**Depends on:** Auth / browser-session surface (multi-tenant, backlog) — the exploit is latent until then + +## Problem + +CORS is configured with `allow_credentials=True` and an operator-configurable origin list: + +`backend/app/main.py:195-207` +```python +_cors_origins = [o.strip() for o in get_settings().cors_allow_origins.split(",") if o.strip()] +if _cors_origins: + app.add_middleware( + CORSMiddleware, + allow_origins=_cors_origins, + allow_credentials=True, + ... + ) +``` + +The MVP1 default (`http://localhost:3000,http://127.0.0.1:3000`) is safe, and `allow_origins=["*"]` combined with `allow_credentials=True` is actually neutralized by Starlette/the CORS spec (a literal `*` is not reflected when credentials are allowed). The real footgun is a future operator setting a **broad but non-`*` origin** (e.g. a wildcard-ish entry, or pasting in a list that includes an attacker-influencable origin) while `allow_credentials=True` stays hardcoded. Once RelyLoop has any browser-bound credential (cookie/session — arrives with the multi-tenant auth surface, currently backlog), that combination lets a malicious origin in the list make credentialed cross-origin requests and read the responses. + +Today there is **no auth and no browser credential**, so there is nothing for `allow_credentials=True` to leak — this is a **latent** hardening item, not a live vulnerability. It is filed so the footgun is closed *before* the auth surface that arms it lands, not after. + +## Proposed capabilities + +### Make the credentials/origins combination safe by construction + +- When the auth surface lands, validate the CORS config at startup: reject (fail-fast) any wildcard/over-broad origin entry while `allow_credentials=True`, or decouple credentialed CORS from the public-origin list entirely. +- Consider gating `allow_credentials` on whether a browser-session credential actually exists (it does not in MVP1), so the flag is not "on" ahead of need. +- Document the safe configuration in `docs/04_security/` and `docs/01_architecture/deployment.md`. + +## Scope signals + +- **Backend:** `backend/app/main.py` CORS setup + a startup validation helper; a unit/contract test asserting the rejected combinations. +- **Frontend:** none. +- **Migration:** none. +- **Config:** `CORS_ALLOW_ORIGINS` semantics documented; possibly a new `CORS_ALLOW_CREDENTIALS` toggle. +- **Audit events:** N/A. + +## Why filed as an idea (and in 04_ga) rather than fixed inline + +The exploit is only reachable once browser-bound credentials/auth exist, which is the multi-tenant/SSO surface deferred to the backlog/GA — so this is a **different-release** hardening concern, not current-MVP work, and it depends on a design (how auth + CORS interact) that does not exist yet. Filing in `04_ga/` (production-readiness hardening) matches where the dependency lands. + +## Relationship to other work + +Part of the security-review idea sweep on branch `claude/codebase-security-review-6njwio`. Couples to the future auth/multi-tenant surface (backlog). Independent of the SSRF, request-ID, agent-confirmation, and test-router siblings.