From d7dc37daba325ebad2a322504a22b9ed6cea50f1 Mon Sep 17 00:00:00 2001 From: Brian McMahon Date: Thu, 10 Sep 2026 09:08:30 -0700 Subject: [PATCH] baseline: SECURITY.md, PR/issue templates, coverage-scope test, CI-sourced badges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes RB-3.2-sec, RB-3.2-pr, RB-3.2-iss, RB-4.2-C5, RB-5.1 from alpha-engine-config-I10436 (RB-2.2/RB-2.2-hp/RB-6 out of scope here). - SECURITY.md: disclosure path (GitHub Security Advisory + email), 72h ack / 14d fix window, PII/tax-document-specific threat model. - .github/pull_request_template.md: model-attribution footer per pull-request-policy.md §3, mirrored from crucible/morning-signal. - .github/ISSUE_TEMPLATE/{bug_report,feature_request}.md, mirrored from morning-signal with telos-specific fields (no-PII reminder, tax year/form). - tests/test_coverage_scope.py: asserts --cov targets the whole telos package, exactly one enforced floor >= the pinned ratchet, no coverage omit beyond the (currently empty) justified set, and no stray source module outside the measured package. - pyproject.toml: coverage floor raised 90 -> 98 as a ratchet, against a measured 98.34% (483 passed, 1 skipped locally). - README.md: hand-set License and Python-version badges replaced. License now reads img.shields.io/github/license/nousergon/telos (generated). Python-version badge dropped outright rather than faked — telos is not published to PyPI (pypi.org/pypi/telos is an unrelated package), so no shields-generated source exists for it. Added a Coverage badge rendering the shields endpoint scripts/publish_coverage_badge.sh publishes. - .github/workflows/ci.yml: badge-publish step on the python-3.13 leg, push-to-main only, job-level `permissions: contents: write` scoped to that one write. - badges branch seeded on the remote before this PR opened (coverage.json = "pending" + machine-written README) so the merge alone is sufficient — no post-merge step. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01T3iQ5BqsPVNPZgQQsYNh2L --- .github/ISSUE_TEMPLATE/bug_report.md | 33 +++++++ .github/ISSUE_TEMPLATE/feature_request.md | 21 +++++ .github/pull_request_template.md | 19 ++++ .github/workflows/ci.yml | 21 +++++ README.md | 6 +- SECURITY.md | 52 +++++++++++ pyproject.toml | 6 +- scripts/publish_coverage_badge.sh | 71 +++++++++++++++ tests/test_coverage_scope.py | 103 ++++++++++++++++++++++ 9 files changed, 328 insertions(+), 4 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md create mode 100644 SECURITY.md create mode 100755 scripts/publish_coverage_badge.sh create mode 100644 tests/test_coverage_scope.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..be8082c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Something isn't working as expected +title: '' +labels: bug +assignees: '' +--- + +**What happened** +A clear description of the bug. + +**What you expected** +What you expected to happen instead. + +**Steps to reproduce** +1. +2. +3. + +**Environment** +- telos version: +- Python version: +- OS: +- Tax year / form(s) involved: + +**Logs / output** +``` +paste any error output or traceback here — never paste real tax data, SSNs, +account numbers, or any other PII +``` + +**Does it reproduce with a synthetic fixture (not your own tax data)?** + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..47b1c4b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Suggest an idea or enhancement +title: '' +labels: enhancement +assignees: '' +--- + +**The problem** +What are you trying to do that Telos doesn't support today? + +**Proposed solution** +What you'd like to see. + +**Alternatives considered** +Other approaches you've thought about. + +**Scope note** +Telos is a deterministic engine: every computed line must be traceable to its inputs and +a primary-source citation. New forms/line items need the citation up front, not derived +from memory. See `CONTRIBUTING.md` for the coverage-guard contract new forms must satisfy. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..01e9b45 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +## What & why + + + +## Checklist + +- [ ] Tests added/updated for the behavior change +- [ ] `pytest` passes locally — the coverage floor in `pyproject.toml` is a ratchet, raised as coverage improves and never lowered to make a change pass +- [ ] `ruff check .` is clean for files I touched +- [ ] No secrets, real tax documents, or real PII committed (synthetic fixtures only) +- [ ] The coverage guard (`telos.engine.guard`) still declares every form/line item this change touches — no silent bypass + +## Test plan + + + +--- + +Prepared by: via [Claude Code](https://claude.com/claude-code) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20140e7..40060cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,11 @@ jobs: test: runs-on: ubuntu-latest + # The badge-publish step below writes `coverage.json` on the orphan + # `badges` branch through the contents API. Nothing else in this job + # writes, and the token is scoped no wider than that. + permissions: + contents: write strategy: fail-fast: false matrix: @@ -38,6 +43,22 @@ jobs: - run: pip install -e ".[dev]" - run: pytest --cov --cov-report=term-missing + # repository-baseline-policy.md §5.1 — a badge whose value is written by + # a human is forbidden: it renders identically to a real one and becomes + # false the moment reality moves without anyone editing a file. This + # repo carried no coverage badge at all. The README badge now renders + # whatever this step last published, derived from the same `.coverage` + # file the pyproject.toml gate reads. + # + # One leg only, pushes to main only: a matrix run on every leg would + # race to publish, and a pull-request run measures a tree that is not + # yet what a reader of the README is looking at. + - name: Publish coverage badge + if: matrix.python-version == '3.13' && github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: bash scripts/publish_coverage_badge.sh + secrets: # raw gitleaks binary — gitleaks-action@v2 requires a paid license on org repos. # Runs on GitHub-hosted ubuntu-latest: this is a PUBLIC repo, so GHA-hosted diff --git a/README.md b/README.md index b523c5e..7c222a2 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ > The tool that completes the year. [![CI](https://github.com/nousergon/telos/actions/workflows/ci.yml/badge.svg)](https://github.com/nousergon/telos/actions/workflows/ci.yml) -[![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE) -[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/) +[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/nousergon/telos/badges/coverage.json)](https://github.com/nousergon/telos/actions/workflows/ci.yml) +[![License](https://img.shields.io/github/license/nousergon/telos)](LICENSE) [![Status: Pre-Alpha](https://img.shields.io/badge/status-pre--alpha-orange.svg)](#status) **A deterministic personal tax engine.** LLM document ingestion in front, pure-code @@ -71,7 +71,7 @@ git clone https://github.com/nousergon/telos.git cd telos python3 -m venv .venv && source .venv/bin/activate pip install -e ".[dev]" -pytest # 88 tests, coverage gate 90% +pytest # 483 tests, coverage gate 98% (measured over the whole telos package) ruff check . ``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..129d1f4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,52 @@ +# Security Policy + +## Reporting a vulnerability + +If you find a security vulnerability in Telos, please report it privately: + +- **Preferred:** open a [GitHub Security Advisory](https://github.com/nousergon/telos/security/advisories/new). This keeps the discussion private until a fix ships. +- **Alternative:** email `security@nousergon.ai` with a description and reproduction steps. + +Please **do not** open a public issue for security reports. I aim to acknowledge within 72 hours and ship a fix or mitigation within 14 days for high-severity issues. + +## Scope + +Telos is a personal, self-hosted tax engine: LLM document ingestion in front, pure-code +computation in the middle, official-form PDF output at the back. The sensitive surface is +**tax-document and PII handling**. In scope: + +- **PII / document exposure:** any path that leaks a W-2, 1099, or other ingested tax + document — or values extracted from one (SSNs, income, account numbers) — through logs, + error messages, telemetry, or a generated artifact that escapes the operator's own + `TELOS_DATA_DIR`. +- **Injection / escalation:** unsafe handling of LLM-extracted document content that + reaches a shell, filesystem path, or the PDF form-filling layer (`src/telos/forms/`). +- **Determinism / correctness escapes:** any path where the coverage guard + (`telos.engine.guard`) is silently bypassed, letting an unsupported form or line item + compute a number without declaring it. +- **Supply-chain:** a dependency or install path that could execute untrusted code during + `pip install -e .` or ingestion. + +Out of scope: + +- Issues requiring local filesystem/process access (if your machine is compromised, the + threat model has already failed — `TELOS_DATA_DIR` and your API keys live there). +- Vulnerabilities in upstream dependencies not yet publicly disclosed — report those + upstream first. + +## Threat model assumptions + +- **Single-user, local-first.** There is no multi-user model in this engine; tax data + stays in the operator's own `TELOS_DATA_DIR`. +- **Credentials for the ingestion path route through the krepis router edge** — no + direct provider API key lives in this repo's runtime config. +- **The model's ingestion output is treated as untrusted structured data**, validated by + Pydantic schemas with `extra="forbid"` before it reaches any downstream computation. +- **HTTPS** is assumed for all router-edge traffic. + +## Hardening recommendations for self-hosters + +- Keep `TELOS_DATA_DIR` and any `.env` at `600` and never commit them. +- Never place real tax documents or fixtures containing real PII into the repo tree — + synthetic fixtures only (`reportlab`-generated fakes in `tests/fixtures/`). +- Set provider-side spend limits on the ingestion path's key. diff --git a/pyproject.toml b/pyproject.toml index 9adc0ea..d3b8b4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,11 @@ markers = [ source = ["telos"] [tool.coverage.report] -fail_under = 90 +# Raised 90 -> 98 on 2026-09-10 (repository-baseline-policy.md §4.2 C3 +# ratchet) against a measured 98.34% — a floor is raised as coverage improves +# and never lowered to make a change pass. tests/test_coverage_scope.py +# protects this value and the measurement scope it applies to. +fail_under = 98 show_missing = true [tool.ruff] diff --git a/scripts/publish_coverage_badge.sh b/scripts/publish_coverage_badge.sh new file mode 100755 index 0000000..f9bb2bd --- /dev/null +++ b/scripts/publish_coverage_badge.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Publish the coverage figure CI just measured as a shields.io endpoint document. +# +# repository-baseline-policy.md §5.1: a badge whose value is written by a human +# is forbidden — it renders identically to a real one, so a reader cannot tell +# them apart, and it becomes false the moment reality moves without anyone +# editing a file. This repo carried no coverage badge at all; the README now +# renders whatever this script last wrote to `coverage.json` on the orphan +# `badges` branch. The number therefore comes from the same `.coverage` file +# the gate in pyproject.toml's [tool.coverage.report] fail_under exits +# non-zero on, and it moves on its own. +# +# Runs from .github/workflows/ci.yml on pushes to main only, from the +# python-3.13 leg. The `badges` branch is seeded before this script is ever +# invoked, so the contents API is enough and no branch is ever created here. +set -euo pipefail + +: "${GH_TOKEN:?GH_TOKEN is required to publish the badge document}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" + +BRANCH="badges" +DOC="coverage.json" + +# The same measurement the gate uses — coverage.py's own totals over the +# [tool.coverage.run] source configured in pyproject.toml, not a figure +# re-derived from parsed stdout. +python -m coverage json -o coverage-summary.json --quiet + +PCT="$(python -c "import json;print(f\"{json.load(open('coverage-summary.json'))['totals']['percent_covered']:.2f}\")")" + +# shields' own convention: red below 60, yellow below 80, green above 90. +COLOR="$(python -c " +pct = float('${PCT}') +print('brightgreen' if pct >= 90 else 'green' if pct >= 80 else 'yellow' if pct >= 60 else 'red') +")" + +python - "$PCT" "$COLOR" <<'PY' > badge-endpoint.json +import json, sys +pct, color = sys.argv[1], sys.argv[2] +print(json.dumps({ + "schemaVersion": 1, + "label": "coverage", + "message": f"{pct}%", + "color": color, +}, indent=2)) +PY + +echo "measured coverage: ${PCT}% (${COLOR})" + +# The contents API needs the blob sha to replace an existing file. Its absence +# is a hard error rather than a create, because a missing document means the +# `badges` branch is gone — and silently recreating it would hide that the +# README badge has been rendering an error to every reader in the meantime. +SHA="$(gh api "repos/${GITHUB_REPOSITORY}/contents/${DOC}?ref=${BRANCH}" --jq '.sha')" + +if [ -z "${SHA}" ]; then + echo "::error::${DOC} not found on the ${BRANCH} branch — the README badge is broken" >&2 + exit 1 +fi + +if [ "$(gh api "repos/${GITHUB_REPOSITORY}/contents/${DOC}?ref=${BRANCH}" --jq '.content' | base64 --decode)" = "$(cat badge-endpoint.json)" ]; then + echo "coverage unchanged at ${PCT}% — nothing to publish" + exit 0 +fi + +gh api --method PUT "repos/${GITHUB_REPOSITORY}/contents/${DOC}" \ + -f message="chore(badges): coverage ${PCT}% from ${GITHUB_SHA:-HEAD}" \ + -f branch="${BRANCH}" \ + -f sha="${SHA}" \ + -f content="$(base64 < badge-endpoint.json | tr -d '\n')" \ + --jq '.commit.sha' diff --git a/tests/test_coverage_scope.py b/tests/test_coverage_scope.py new file mode 100644 index 0000000..8fd60a3 --- /dev/null +++ b/tests/test_coverage_scope.py @@ -0,0 +1,103 @@ +"""The coverage gate's *scope* is asserted here, not only its number. + +repository-baseline-policy.md §4.2 C5: the way a coverage gate stops being +honest is by narrowing what it measures rather than by lowering the number — +which reads as an improvement in every report. Measured on symposion, removing +one flag moved the reported figure from 34.76% to 92.36% with no new test +code. + +So these tests assert what a passing suite cannot otherwise notice: + +* the measured source is the WHOLE ``telos`` package (C1), never a path or + submodule narrower than that; +* the floor is enforced by a non-zero exit (C2) and is a ratchet that may be + raised and never lowered (C3); +* no coverage `omit` beyond a pinned, empty, justified list — telos currently + omits nothing; +* every source module under ``src/telos`` is inside the measured package. +""" + +from __future__ import annotations + +import tomllib +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +PYPROJECT = REPO_ROOT / "pyproject.toml" +PACKAGE_ROOT = REPO_ROOT / "src" / "telos" + +#: The floor may be RAISED here as coverage improves. Lowering it is a policy +#: amendment (repository-baseline-policy.md §4.2 C3), not a code change. +MINIMUM_FLOOR = 98 + +#: No source is currently omitted from measurement. Widening this set is a +#: scope decision, not a drive-by coverage bump — update this list alongside +#: an [tool.coverage.run] comment justifying each entry if it ever grows. +EXPECTED_OMIT: set[str] = set() + + +def _pyproject() -> dict: + return tomllib.loads(PYPROJECT.read_text(encoding="utf-8")) + + +def test_coverage_source_is_the_whole_package() -> None: + """C1 — ``source`` names the package root, so unimported modules still count.""" + sources = _pyproject()["tool"]["coverage"]["run"]["source"] + assert sources == ["telos"], ( + f"coverage source must be exactly the telos package, got {sources!r}. " + "Narrowing it to a submodule or a path measures the tested subset and " + "reports it as the repository." + ) + + +def test_coverage_floor_is_enforced_and_never_lowered() -> None: + """C2 + C3 — the gate exits non-zero below a floor that only ratchets up.""" + fail_under = _pyproject()["tool"]["coverage"]["report"]["fail_under"] + assert isinstance(fail_under, int), ( + f"fail_under must be a single integer floor, got {fail_under!r}" + ) + assert fail_under >= MINIMUM_FLOOR, ( + f"coverage floor {fail_under} is below the ratchet {MINIMUM_FLOOR}. " + "A floor is raised as coverage improves and never lowered to make a " + "change pass (repository-baseline-policy.md §4.2 C3)." + ) + + +def test_coverage_omit_matches_the_pinned_justified_set() -> None: + """A shrunk denominator is a narrowing this test forces into review.""" + omit = set(_pyproject()["tool"]["coverage"]["run"].get("omit", [])) + added = omit - EXPECTED_OMIT + assert not added, ( + f"coverage omit gained unreviewed entries: {sorted(added)}. " + "Each omitted path removes files from the denominator, raising the " + "reported figure without adding a test — update EXPECTED_OMIT here " + "alongside a justification comment in pyproject.toml if this is " + "deliberate." + ) + + +def test_every_source_module_is_inside_the_measured_package() -> None: + """No source file lives outside what ``--cov=telos`` measures.""" + src = REPO_ROOT / "src" + stray = sorted( + p.relative_to(REPO_ROOT).as_posix() + for p in src.rglob("*.py") + if PACKAGE_ROOT not in p.parents and p != PACKAGE_ROOT + ) + assert not stray, ( + f"source modules outside the measured package are invisible to the " + f"coverage gate: {stray}" + ) + + +def test_no_cov_fail_under_flag_shadows_the_pyproject_gate() -> None: + """A CI-passed --cov-fail-under could silently override pyproject.toml's.""" + import re + + for workflow in (REPO_ROOT / ".github" / "workflows").glob("*.yml"): + text = workflow.read_text(encoding="utf-8") + for match in re.findall(r"--cov-fail-under=(\d+)", text): + assert int(match) >= MINIMUM_FLOOR, ( + f"{workflow.name} passes --cov-fail-under={match} directly, " + "bypassing the pyproject.toml ratchet this test protects." + )