phase: 121_git_source_tokens
Build and Push Containers / build-and-push-app (push) Successful in 2m3s
Build and Push Containers / build-and-push-db (push) Failing after 14s

**Phase 121 final verification pass — all green** (all 4 tasks already in `complete/`; verified, no defects found, no changes needed)

- Verified implementation vs phase design: migration `0021` (reversible, round-tripped via `alembic downgrade base` + `upgrade head` → head `0021`), `GitSource.token` column, `normalize_credential`/`clone_url_for`/`sanitize_url`, clone callers switched (`sync.py`, `import_docs.py`), masked token fields in add form + editor, `extra="forbid"` output shapes
- Tests: `uv run pytest` → 2662 passed, 0 failed (exit 0); `uv run pytest --cov=app --cov-report=term-missing` → TOTAL **99%** (≥90% gate)
- Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors, 0 warnings
- E2E in isolation: `uv run pytest tests/e2e/test_git_source_tokens.py -v --no-cov` → **4 passed**

Completion criteria:
1. Private repo (UI add or pasted embedded-token URL) clones with injected token; token absent from every API response, page text, title attr, and full HTML — **PASS** (integration raw-JSON assertions + E2E `_assert_token_nowhere`)
2. Legacy embedded-token rows still clone from stored URL; output sanitized — **PASS** (`test_sync_legacy_row_clones_with_original_stored_url`, `test_get_masks_legacy_embedded_token_row`, env-fallback masking)
3. Public/local sources byte-identical — **PASS** (verbatim-URL + no-userinfo-unchanged tests)
4. pytest / coverage / ruff / pyright — **PASS** (see above)
5. Commit + phase move — harness responsibility; task files already in `complete/`, changes left in working tree (no commit made, per protocol)

Notable: no deviations; DB left at head, functional. Next pending phase: **122_image_documents** (then 123_chat_image_questions).
This commit is contained in:
2026-09-24 20:51:39 -04:00
parent 3a0fc3db05
commit 0f77e9a876
35 changed files with 2894 additions and 48 deletions
+37 -5
View File
@@ -625,10 +625,11 @@
to the database.
</p>
<!-- Add form: visible label + mono URL input + brand button
(dark ink on brand 5.2:1). §7.4 never-stale: the button
disables + relabels "Adding…" while the POST is in flight
and re-enables on success AND failure (the input is kept
<!-- Add form: visible label + mono URL input + the optional
masked token field (phase 121) + brand button (dark ink
on brand 5.2:1). §7.4 never-stale: the button disables +
relabels "Adding…" while the POST is in flight and
re-enables on success AND failure (the inputs are kept
on failure, same as the tuning forms). -->
<form id="git-source-form">
<label for="git-source-url">Add a git source</label>
@@ -641,6 +642,22 @@
placeholder="https://github.com/you/your-repo.git"
required
>
<!-- Phase 121 (task 03, LOCKED A2): the optional masked
private-repo credential — type=password +
autocomplete=off (a PAT is not a site credential: no
browser save offer). Blank = the POST omits the key
(None = no credential); the token is write-only — it
never round-trips (the API shapes carry no token
field). -->
<label for="git-source-token">Token <span class="field-hint">optional — private repos</span></label>
<input
id="git-source-token"
name="token"
type="password"
maxlength="500"
autocomplete="off"
placeholder="ghp_… or another PAT"
>
<button type="submit" id="git-source-add">Add source</button>
<p class="git-source-error" id="git-source-error" role="alert" hidden></p>
</form>
@@ -784,12 +801,16 @@
while the PATCH is out). Stored rows only (A3) — the
per-row button lives in git-sources.js's makeRow; this
is the page-local dialog it opens. -->
<!-- Phase 121 (task 03): the per-row editor gains the
optional masked token field (LOCKED A2) — the title
widens from "Ignored files and folders" to the dialog's
actual scope (the row's settings). -->
<div class="ignore-editor" id="ignore-editor-dialog" role="alertdialog"
aria-modal="true" aria-labelledby="ignore-editor-title"
aria-describedby="ignore-editor-copy" hidden>
<div class="ignore-editor-backdrop" aria-hidden="true"></div>
<div class="ignore-editor-panel">
<h2 class="ignore-editor-title" id="ignore-editor-title">Ignored files and folders</h2>
<h2 class="ignore-editor-title" id="ignore-editor-title">Source settings</h2>
<code class="ignore-editor-source" id="ignore-editor-source"></code>
<p class="ignore-editor-copy" id="ignore-editor-copy">
One path per line. A file is ignored when its path in the
@@ -801,6 +822,17 @@
<textarea class="ignore-editor-textarea" id="ignore-editor-textarea"
rows="6" spellcheck="false"
placeholder="my/files/"></textarea>
<!-- Phase 121 (task 03, LOCKED A2): the masked
private-repo credential — type=password +
autocomplete=off (a PAT is not a site credential).
BLANK = the PATCH omits the key (the tri-state: no
change — the row's stored token is kept). The field
is NEVER prefilled (the API has no token field to
read it from). -->
<label class="ignore-editor-label ignore-editor-token-label" for="ignore-editor-token">Token <span class="field-hint">optional — private repos</span></label>
<input class="ignore-editor-token" id="ignore-editor-token"
type="password" maxlength="500" autocomplete="off"
placeholder="leave blank to keep the current token">
<p class="ignore-editor-error" id="ignore-editor-error" role="alert" hidden></p>
<div class="ignore-editor-actions">
<button type="button" class="ignore-editor-btn ignore-editor-cancel"