From 3cc44d5697209cbf058d6a9efc7d67e496c4af78 Mon Sep 17 00:00:00 2001 From: McAtk <16798627+atk0309@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:41:16 +0100 Subject: [PATCH] chore: harden public repository health --- .github/ISSUE_TEMPLATE/bug_report.yml | 82 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 ++ .github/ISSUE_TEMPLATE/feature_request.yml | 54 ++++++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 26 +++++++ .github/workflows/codeql.yml | 48 +++++++++++++ CODE_OF_CONDUCT.md | 42 +++++++++++ README.md | 3 +- backend/pyproject.toml | 1 + backend/tests/test_applications.py | 6 +- backend/tests/test_corrector.py | 6 +- backend/tests/test_cv.py | 10 +-- backend/tests/test_ingest.py | 18 ++--- backend/tests/test_master.py | 6 +- backend/tests/test_review.py | 19 ++--- backend/uv.lock | 39 ++++++++++ docs/agent-guidance-changelog.md | 13 ++++ docs/architecture/deployment.md | 11 ++- docs/testing/strategy.md | 12 +++- frontend/package.json | 6 +- frontend/vitest.config.ts | 3 + 20 files changed, 368 insertions(+), 42 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/codeql.yml create mode 100644 CODE_OF_CONDUCT.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..c692185 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,82 @@ +name: Bug report +description: Report a reproducible problem in ContenderOS +title: "[Bug]: " +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for helping improve ContenderOS. Use synthetic or redacted examples only. + Security vulnerabilities and personal-data exposures belong in the private + **Report a vulnerability** flow, not a public issue. + + - type: checkboxes + id: preflight + attributes: + label: Before submitting + options: + - label: I searched existing issues for this problem. + required: true + - label: I removed credentials, real CV data, contact details, cookies, and account identifiers. + required: true + - label: This is not a security vulnerability or personal-data exposure. + required: true + + - type: textarea + id: summary + attributes: + label: Problem + description: What went wrong, and what user-facing impact did it have? + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Minimal reproduction + description: Give the smallest reliable sequence using synthetic data. + placeholder: | + 1. Start ... + 2. Open ... + 3. Observe ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behaviour + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual behaviour + validations: + required: true + + - type: input + id: version + attributes: + label: Version or commit + description: Release, branch, or commit SHA where the problem occurs. + placeholder: main at abc1234 + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: OS, browser, Node/Python versions, and deployment type if relevant. + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Sanitised logs or screenshots + description: Optional. Redact secrets, URLs with tokens, and all personal data. + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e8bb527 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Report a security or personal-data exposure + url: https://github.com/atk0309/project_ContenderOS/security/advisories/new + about: Use the private reporting flow; never disclose secrets or personal data in a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..d1c3d01 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,54 @@ +name: Feature request +description: Propose a focused improvement to ContenderOS +title: "[Feature]: " +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Explain the user problem before prescribing an implementation. Proposals must preserve + the invariants in `AGENTS.md`, especially provenance, deterministic rendering, structured + outputs, server-side secrets, and tenant isolation. + + - type: checkboxes + id: preflight + attributes: + label: Before submitting + options: + - label: I searched existing issues for a similar proposal. + required: true + - label: The example contains no real CV, recruiter, account, or credential data. + required: true + + - type: textarea + id: problem + attributes: + label: User problem + description: Who is affected, and what are they unable to do today? + validations: + required: true + + - type: textarea + id: outcome + attributes: + label: Desired outcome + description: Describe observable behaviour, not an internal architecture. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Include simpler workflows or existing features that might solve the problem. + validations: + required: true + + - type: textarea + id: invariant_impact + attributes: + label: Invariant and privacy impact + description: Note any effect on provenance, export sign-off, rendering, providers, auth, or tenancy. + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..65c2ee4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ +## What changed + +Describe the behaviour change and why it is needed. Link the issue when one exists. + +## Validation + +- [ ] I ran `node scripts/quality-gate.mjs --fail-on-gap` and it passed. +- [ ] I listed any intentionally skipped live-provider or Postgres checks below. +- [ ] For a non-trivial UI/render change, I verified the affected routes and states with Playwright + and checked preview/PDF parity. + +Commands and results: + + + +## Safety and documentation + +- [ ] This change preserves the invariants in `AGENTS.md`. +- [ ] Tests cover the changed behaviour, including deny/failure cases where relevant. +- [ ] Documentation was updated according to the doc-impact checklist, or no rule was triggered. +- [ ] Generated OpenAPI/TypeScript files are in sync when backend schemas changed. +- [ ] The diff contains no credentials, personal data, generated CVs, databases, or populated env files. + +## Known risks or follow-ups + + diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..d023452 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,48 @@ +name: codeql + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "17 4 * * 1" + workflow_dispatch: + +concurrency: + group: codeql-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + +jobs: + analyze: + name: CodeQL (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + packages: read + security-events: write + strategy: + fail-fast: false + matrix: + language: + - javascript-typescript + - python + + steps: + - uses: actions/checkout@v7 + + - name: Initialise CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: none + queries: security-extended + + - name: Analyse + uses: github/codeql-action/analyze@v4 + with: + category: /language:${{ matrix.language }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ffbe973 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,42 @@ +# Code of Conduct + +## Our commitment + +ContenderOS is a place for constructive, technically rigorous collaboration. We welcome +contributors regardless of background, identity, experience level, or perspective, and we are +committed to keeping project spaces respectful and free from harassment. + +## Expected behaviour + +- Be considerate, specific, and kind when discussing people or their work. +- Critique ideas and code, not the person presenting them. +- Assume good intent while accepting correction when impact differs from intent. +- Respect privacy. Never post another person's CV, contact details, credentials, account data, or + private correspondence. +- Keep feedback relevant to the project and make room for less-experienced contributors. + +## Unacceptable behaviour + +- Harassment, threats, intimidation, stalking, or sustained disruption. +- Discriminatory, sexualised, insulting, or demeaning language or imagery. +- Publishing private information without explicit permission. +- Deliberate exposure of secrets, personal records, or security reports. +- Retaliation against someone who raises a good-faith concern. + +## Scope + +This policy applies in repository issues, pull requests, reviews, discussions, and other spaces +where someone is representing ContenderOS. + +## Reporting and enforcement + +Do not report sensitive conduct concerns in a public issue. Use the repository's private +**Report a vulnerability** flow and prefix the report title with `[Conduct]`; this provides a +private repository-local channel to the maintainer. If the concern involves the maintainer or you +do not want them to receive the report, use +[GitHub's abuse-reporting process](https://docs.github.com/en/site-policy/acceptable-use-policies/github-bullying-and-harassment). + +Reports will be handled as confidentially as practical. Maintainers may remove content, issue a +private correction or warning, temporarily restrict participation, or permanently ban a +participant, depending on severity and pattern. Knowingly false or retaliatory reports are also a +violation; a good-faith report that cannot be substantiated is not. diff --git a/README.md b/README.md index 90fa58c..3854b19 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,8 @@ Tracked fixtures and design examples are synthetic. Keep the public repository t ## Contributing Read [CONTRIBUTING.md](CONTRIBUTING.md) and the repository's [AGENTS.md](AGENTS.md) before changing -code. The local quality gate is required for every contribution. +code. The local quality gate is required for every contribution, and participation is governed by +the [Code of Conduct](CODE_OF_CONDUCT.md). ## License diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 41c04a6..9649fe3 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -26,6 +26,7 @@ dev = [ "mypy>=2.3.0", "pytest>=9.1.1", "aiosqlite>=0.20", # offline test DB (sqlite+aiosqlite) so the suite needs no Postgres + "httpx2>=2.7.0", # Starlette 1.3 TestClient; runtime Authlib/Resend calls still use httpx ] # Virtual project: uv never builds/installs it as a package (the Dockerfile syncs with diff --git a/backend/tests/test_applications.py b/backend/tests/test_applications.py index c4cc837..748313b 100644 --- a/backend/tests/test_applications.py +++ b/backend/tests/test_applications.py @@ -14,7 +14,7 @@ from collections.abc import Iterator from typing import Any, TypeVar -import httpx +import httpx2 import pytest from app.ai.agents.registry import get_model_resolver, get_runner from app.auth.deps import current_user @@ -188,14 +188,14 @@ def _apply( company: str | None = None, jobUrl: str | None = None, note: str | None = None, -) -> httpx.Response: +) -> httpx2.Response: bv = _version(client, cv_id, workspace) if base_version is None else base_version body: dict[str, object] = {"cvId": cv_id, "baseVersion": bv} extras = (("jobTitle", jobTitle), ("company", company), ("jobUrl", jobUrl), ("note", note)) for key, val in extras: if val is not None: body[key] = val - res: httpx.Response = client.post(f"/workspaces/{workspace}/applications", json=body) + res: httpx2.Response = client.post(f"/workspaces/{workspace}/applications", json=body) return res diff --git a/backend/tests/test_corrector.py b/backend/tests/test_corrector.py index 54e3f0f..266af31 100644 --- a/backend/tests/test_corrector.py +++ b/backend/tests/test_corrector.py @@ -21,7 +21,7 @@ from collections.abc import Iterator from typing import Any, TypeVar -import httpx +import httpx2 import pytest from app.ai.agents.registry import ( get_authoring_runner_builder, @@ -213,9 +213,9 @@ def _apply( *, base_version: int | None = None, workspace: str = "default", -) -> httpx.Response: +) -> httpx2.Response: bv = _version(client, cv_id, workspace) if base_version is None else base_version - res: httpx.Response = client.post( + res: httpx2.Response = client.post( f"/workspaces/{workspace}/cv/{cv_id}/apply", json={"threads": threads, "baseVersion": bv}, ) diff --git a/backend/tests/test_cv.py b/backend/tests/test_cv.py index 0fa7044..fbf19ce 100644 --- a/backend/tests/test_cv.py +++ b/backend/tests/test_cv.py @@ -10,7 +10,7 @@ from collections.abc import Iterator from typing import TypeVar -import httpx +import httpx2 import pytest from app.ai.agents.registry import get_model_resolver, get_runner from app.auth.deps import current_user @@ -151,9 +151,9 @@ def _signoff( *, base_version: int | None = None, workspace: str = "default", -) -> httpx.Response: +) -> httpx2.Response: bv = _version(client, cv_id, workspace) if base_version is None else base_version - res: httpx.Response = client.post( + res: httpx2.Response = client.post( f"/workspaces/{workspace}/cv/{cv_id}/signoff", json={"bulletIds": bullet_ids, "baseVersion": bv}, ) @@ -168,9 +168,9 @@ def _edit( *, base_version: int | None = None, workspace: str = "default", -) -> httpx.Response: +) -> httpx2.Response: bv = _version(client, cv_id, workspace) if base_version is None else base_version - res: httpx.Response = client.post( + res: httpx2.Response = client.post( f"/workspaces/{workspace}/cv/{cv_id}/edit", json={"bulletId": bullet_id, "text": text, "baseVersion": bv}, ) diff --git a/backend/tests/test_ingest.py b/backend/tests/test_ingest.py index df68c1a..7150fa7 100644 --- a/backend/tests/test_ingest.py +++ b/backend/tests/test_ingest.py @@ -14,7 +14,7 @@ from collections.abc import Callable, Iterator from typing import Any, TypeVar -import httpx +import httpx2 import pytest from app.ai import pdf_text from app.ai.agents.extractor import ( @@ -265,23 +265,23 @@ def client(sqlite_maker: async_sessionmaker[AsyncSession]) -> Iterator[TestClien app.dependency_overrides.clear() -def _start(client: TestClient, text: str = "My CV text", ws: str = "default") -> httpx.Response: - res: httpx.Response = client.post(f"/workspaces/{ws}/ingest", json={"text": text}) +def _start(client: TestClient, text: str = "My CV text", ws: str = "default") -> httpx2.Response: + res: httpx2.Response = client.post(f"/workspaces/{ws}/ingest", json={"text": text}) return res def _bulk( client: TestClient, run_id: str, proposals: list[dict[str, Any]], action: str = "accept" -) -> httpx.Response: +) -> httpx2.Response: items = [{"proposalId": p["proposalId"], "baseVersion": p["version"]} for p in proposals] - res: httpx.Response = client.post( + res: httpx2.Response = client.post( f"/workspaces/default/ingest/{run_id}/decide", json={"items": items, "action": action} ) return res -def _apply(client: TestClient, run_id: str) -> httpx.Response: - res: httpx.Response = client.post(f"/workspaces/default/ingest/{run_id}/apply") +def _apply(client: TestClient, run_id: str) -> httpx2.Response: + res: httpx2.Response = client.post(f"/workspaces/default/ingest/{run_id}/apply") return res @@ -732,8 +732,8 @@ def _upload( content_type: str = "application/pdf", source_type: str = "cv-pdf", ws: str = "default", -) -> httpx.Response: - res: httpx.Response = client.post( +) -> httpx2.Response: + res: httpx2.Response = client.post( f"/workspaces/{ws}/ingest/file", files={"file": (filename, _make_pdf() if content is None else content, content_type)}, data={"sourceType": source_type}, diff --git a/backend/tests/test_master.py b/backend/tests/test_master.py index 8c9e626..d006141 100644 --- a/backend/tests/test_master.py +++ b/backend/tests/test_master.py @@ -9,7 +9,7 @@ import os from collections.abc import Iterator -import httpx +import httpx2 import pytest from app.auth.deps import current_user from app.main import app @@ -43,9 +43,9 @@ def client(sqlite_maker: async_sessionmaker[AsyncSession]) -> Iterator[TestClien def _put( client: TestClient, master: MasterProfile | None = None, workspace: str = "default" -) -> httpx.Response: +) -> httpx2.Response: payload = (master or sample_master_profile()).model_dump(by_alias=True) - res: httpx.Response = client.put(f"/workspaces/{workspace}/master", json=payload) + res: httpx2.Response = client.put(f"/workspaces/{workspace}/master", json=payload) return res diff --git a/backend/tests/test_review.py b/backend/tests/test_review.py index b1e0bfa..b4bddbf 100644 --- a/backend/tests/test_review.py +++ b/backend/tests/test_review.py @@ -25,6 +25,7 @@ import app.ai.agents.registry as registry import httpx +import httpx2 import pytest from anthropic import APIError as AnthropicAPIError from app.ai.agents.fact_checker import apply_fact_check_authority @@ -260,12 +261,12 @@ def _review( cv_id: str, workspace: str = "default", base_version: int | None = None, -) -> httpx.Response: +) -> httpx2.Response: """Blocking review (no `Accept`). `baseVersion` defaults to the stored CV's current version.""" bv = base_version if bv is None: bv = int(client.get(f"/workspaces/{workspace}/cv/{cv_id}").json()["cv"]["version"]) - res: httpx.Response = client.post( + res: httpx2.Response = client.post( f"/workspaces/{workspace}/cv/{cv_id}/review", json={"baseVersion": bv} ) return res @@ -273,7 +274,7 @@ def _review( def _stream_review( client: TestClient, cv_id: str, workspace: str = "default", base_version: int = 1 -) -> tuple[list[dict[str, object]], httpx.Headers]: +) -> tuple[list[dict[str, object]], httpx2.Headers]: """Drive the SSE arm; collect parsed events up to and including `run_done`. Returns the events + the response headers (so a test can assert there is no `X-Run-Id`).""" events: list[dict[str, object]] = [] @@ -313,8 +314,8 @@ def test_review_cross_tenant_404(client: TestClient) -> None: # (`_get_review` is defined below in the persisted-review section; reused here.) -def _delete_review(client: TestClient, cv_id: str, workspace: str = "default") -> httpx.Response: - res: httpx.Response = client.delete(f"/workspaces/{workspace}/cv/{cv_id}/review") +def _delete_review(client: TestClient, cv_id: str, workspace: str = "default") -> httpx2.Response: + res: httpx2.Response = client.delete(f"/workspaces/{workspace}/cv/{cv_id}/review") return res @@ -813,8 +814,8 @@ def test_fact_check_authority_grounds_sourceid_per_item() -> None: # --- persisted review runs (ADR 0013) ----------------------------------------------------------- -def _get_review(client: TestClient, cv_id: str, workspace: str = "default") -> httpx.Response: - res: httpx.Response = client.get(f"/workspaces/{workspace}/cv/{cv_id}/review") +def _get_review(client: TestClient, cv_id: str, workspace: str = "default") -> httpx2.Response: + res: httpx2.Response = client.get(f"/workspaces/{workspace}/cv/{cv_id}/review") return res @@ -824,8 +825,8 @@ def _put_decisions( decisions: dict[str, str], cv_version: int, workspace: str = "default", -) -> httpx.Response: - res: httpx.Response = client.put( +) -> httpx2.Response: + res: httpx2.Response = client.put( f"/workspaces/{workspace}/cv/{cv_id}/review/decisions", json={"decisions": decisions, "cvVersion": cv_version}, ) diff --git a/backend/uv.lock b/backend/uv.lock index 57f2ad9..b31c0d1 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -255,6 +255,7 @@ dependencies = [ [package.optional-dependencies] dev = [ { name = "aiosqlite" }, + { name = "httpx2" }, { name = "mypy" }, { name = "pytest" }, { name = "ruff" }, @@ -269,6 +270,7 @@ requires-dist = [ { name = "cryptography", specifier = ">=43" }, { name = "fastapi", specifier = ">=0.139.2" }, { name = "httpx", specifier = ">=0.27" }, + { name = "httpx2", marker = "extra == 'dev'", specifier = ">=2.7.0" }, { name = "itsdangerous", specifier = ">=2.2" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=2.3.0" }, { name = "openai", specifier = ">=2.47.0" }, @@ -436,6 +438,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/fe/6a3f9f1a8bb8733326140737446aaf72fddb8b54b8f202302f5c84960613/httpcore2-2.7.0.tar.gz", hash = "sha256:6dc0fedf329a52a990930a5579edfebaea81118ea700ea0dd7de2b5e5be49efc", size = 65593, upload-time = "2026-07-14T20:40:01.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/6c/62e2e279e63fc4f7a5ee841ef13175a8bbc613f258e9dcc186e9de803a42/httpcore2-2.7.0-py3-none-any.whl", hash = "sha256:1452f589fe23f55b44546cd884294c41a29330af902bc0b71a761fd52d18f92b", size = 81506, upload-time = "2026-07-14T20:39:58.053Z" }, +] + [[package]] name = "httptools" version = "0.8.0" @@ -473,6 +488,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx2" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpcore2" }, + { name = "idna" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/4a/129b2e21b90ac2985d3928d96792bccc39bc6dfe796c5eee2d8ec06d4105/httpx2-2.7.0.tar.gz", hash = "sha256:8b30709aed5c8465b0dd3b95c09ce301c8f79e7e7a2d00ab0af551e0d0375b07", size = 94487, upload-time = "2026-07-14T20:40:02.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/b8/c341bba6411bdfda786020343c47a75ef472f6085caf82391b142b1a3ad9/httpx2-2.7.0-py3-none-any.whl", hash = "sha256:ed2a2719c696789e09493bd8e2bec3d8bd925cc6e26b68389ec25ade132f7bf4", size = 90234, upload-time = "2026-07-14T20:39:59.531Z" }, +] + [[package]] name = "idna" version = "3.18" @@ -916,6 +946,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/21/99a0cdaf54eb35e77623c41b5a2c9472ee4404bba687052791fe2aba6773/tqdm-4.69.0-py3-none-any.whl", hash = "sha256:9979978912be667a6ef21fd5d8abf54e324e63d82f7f43c360792ebc2bc4e622", size = 676680, upload-time = "2026-07-17T18:09:04.172Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" diff --git a/docs/agent-guidance-changelog.md b/docs/agent-guidance-changelog.md index 27713b0..b9698d7 100644 --- a/docs/agent-guidance-changelog.md +++ b/docs/agent-guidance-changelog.md @@ -3,6 +3,19 @@ Durable changes to `AGENTS.md`, nested agent guidance, `CLAUDE.md`, repository scripts, and GitHub workflows are recorded here. The local `guidance-self-update` rule enforces that contract. +## 2026-07-23 (public repository health hardening) + +- Added the missing CodeQL workflow for Python and JavaScript/TypeScript so the active `main` + ruleset's CodeQL requirement has a real producer on PRs, `main`, and a weekly schedule. +- Added a code of conduct, structured privacy-safe issue forms, and a pull-request checklist to + complete the public contribution surface. +- Kept the local gate warning-free on the pinned toolchain: backend route tests use Starlette's + `httpx2` TestClient path, while Next and Vitest workers disable Node 26's unused experimental web + storage. + +Why for future agents: keep the public community files privacy-first, keep CodeQL aligned with the +languages and the `main` ruleset, and never satisfy a missing security check by weakening the rule. + ## 2026-07-23 (dependency toolchain refresh) - Moved CI to `astral-sh/setup-uv@v9.0.0` (the release has no floating `v9` ref), pinned uv diff --git a/docs/architecture/deployment.md b/docs/architecture/deployment.md index e0c65f2..234a0a7 100644 --- a/docs/architecture/deployment.md +++ b/docs/architecture/deployment.md @@ -1,7 +1,7 @@ # Deployment Status: active — seed doc -Last updated: 2026-07-03 +Last updated: 2026-07-23 Update when a `*/.env.example` changes (`env-vars-changed`). @@ -92,11 +92,16 @@ use it return **503** ("configure the provider in the admin menu") — correct f the schema no-drift check) on every PR and on push to `main`. **CI does not deploy** — it's the safety net. A Railway project deploys only after an operator connects and configures it. +`.github/workflows/codeql.yml` separately scans both `python` and `javascript-typescript` with the +`security-extended` query suite on PRs, pushes to `main`, and a weekly schedule. Keep that workflow +enabled anywhere the `main` ruleset requires CodeQL; a CodeQL rule with no producing workflow leaves +the repository protected in name but without an analysis result. + To make deploys to `main` trustworthy, do **both**: 1. **Branch protection on `main`** (GitHub → Settings → Branches → add rule for `main`): require a - pull request before merging, require the **`gate`** status check to pass, and block direct/force - pushes. Now red code cannot reach `main`. + pull request before merging, require the **`gate`** status check and CodeQL analysis to pass, and + block direct/force pushes. Now red code cannot reach `main`. 2. **Railway: wait for CI** — in the Railway service settings, enable "Wait for CI" / check-suite gating so Railway only deploys a commit whose GitHub checks passed. (Belt-and-braces for any direct-to-`main` path.) diff --git a/docs/testing/strategy.md b/docs/testing/strategy.md index 55a598a..3c08144 100644 --- a/docs/testing/strategy.md +++ b/docs/testing/strategy.md @@ -1,7 +1,7 @@ # Testing Strategy Status: active -Last updated: 2026-07-03 +Last updated: 2026-07-23 ## Quality gate model @@ -27,7 +27,7 @@ silently ignored. A skipped check is a gap, not a pass. `--fail-on-gap` turns ga | Render | Playwright + `docx` | `frontend/src/render/` | | API types | generated from OpenAPI | `frontend/src/lib/api/generated-types.ts` | | Lint/format | ruff (be) / biome (fe) | `pyproject.toml`, `biome.json` | -| Test | pytest (be) / vitest (fe) | `pyproject.toml`, `vitest.config.ts` | +| Test | pytest + Starlette TestClient via `httpx2` (be) / vitest (fe) | `pyproject.toml`, `vitest.config.ts` | ## Canonical commands @@ -65,7 +65,13 @@ silently ignored. A skipped check is a gap, not a pass. `--fail-on-gap` turns ga `ProviderRunner` **and** a fake model resolver (no live key, no DB, no network). The gated route test overrides `current_user` + `get_runner` + `get_model_resolver` via `app.dependency_overrides`. Also asserts the error mapping: refusal→422, truncation/parse→502, missing key/tier→503 (these - were raw 500s before the handlers landed). Backend AI deps: `anthropic`, `openai`. + were raw 500s before the handlers landed). Backend AI deps: `anthropic`, `openai`. Route tests use + Starlette's `TestClient` through the dev-only `httpx2` package; runtime Authlib and Resend calls + deliberately remain on `httpx`. +- Frontend Next and Vitest worker processes disable Node 26's experimental web-storage global. + The vendored `docx` browser bundle probes `localStorage` during import, but neither build/render + code nor node-environment tests need it; disabling the unused API avoids warning noise and + stateful local-storage files without changing document output. - `backend/tests/test_providers.py` — the native-PDF plumbing (ADR 0007): with a mocked Anthropic client, `run_claude` builds a `document` content block + text block (vs a plain string) and forwards a per-agent `max_tokens`, all without a billed call. The real acceptance of a PDF block + diff --git a/frontend/package.json b/frontend/package.json index 654c6aa..c0d9a22 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,9 +8,9 @@ }, "packageManager": "pnpm@11.16.0", "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", + "dev": "node --no-experimental-webstorage ./node_modules/next/dist/bin/next dev", + "build": "node --no-experimental-webstorage ./node_modules/next/dist/bin/next build", + "start": "node --no-experimental-webstorage ./node_modules/next/dist/bin/next start", "typecheck": "node ./node_modules/typescript-7/bin/tsc --noEmit", "lint": "biome check .", "format": "biome format --write .", diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 287fa37..bf1fe72 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -8,6 +8,9 @@ export default defineConfig({ resolve: { alias: { "@": fileURLToPath(new URL("./src", import.meta.url)) } }, test: { environment: "node", + // docx's browser bundle probes global localStorage. Node 26 exposes experimental web storage + // and warns when no persistence file is configured; test workers need no web storage at all. + execArgv: ["--no-experimental-webstorage"], include: ["src/**/*.test.ts", "src/**/*.test.tsx"], globals: true, },