Commit Graph

7 Commits

Author SHA1 Message Date
Patrick Buckley 3124dbe52f fix(vendor): widen update-vendored-js sweep to catch shared_static/ + .py
The shared_static exclude in scripts/update-vendored-js.sh was meant to
skip self-references inside vendored libraries, but it also hid
shared_static/renderer.js — which loads the vendored libs and pinned
mermaid-11.14.0 across every renovate bump since #426. Tests under
tests/test_web_helpers.py were similarly invisible because the include
list omitted *.py.

Replace the broad shared_static exclude with the specific old-versioned
vendor directory (about to be rm -rf'd next anyway), and add *.py to the
include list. Bump renderer.js to mermaid-11.15.0 to repair the live
404, and refresh the test fixtures to current vendor versions so they
stop drifting.
2026-05-13 16:12:40 -07:00
Patrick Buckley 4b5edce8c5 fix(css): two cascade-flip bugs found by specificity audit (#433)
* fix(css): two cascade-flip bugs found by specificity audit

PR #431 stripped [data-design="v1"] from ~400 rules, dropping each by a
specificity tier; two cascade flips (#header outranking .appbar, #header h1
outranking .appbar-title) were caught visually during that PR's review and
fixed by renaming id="header" → id="ui-header" on the per-node UI page.
This is the audit follow-up; it found two more:

- textarea.skill-content-area (was .skill-content-area) — bumped to (0,1,1)
  so the rule ties with `.admin-modal textarea` (0,1,1) and wins on source
  order. Without the bump, min-height: 220px was clobbered to 40px by the
  modal default and the spec-content textarea rendered short. The three
  !important markers (font-family/size/line-height) are now redundant
  against the modal's font: inherit shorthand and are dropped.

- h3.skill-spec-heading — removed `font-size: inherit;`. The author wrote
  it to "reset UA defaults" but it locked font-size to the parent's
  (~14-16px) at (0,1,1), silently overriding `.skill-spec-heading`'s 10px
  at (0,1,0). The bare class already beats UA `h3` on specificity (class >
  tag), so no font-size reset was needed; the `margin-block: 0` line stays
  because the bare class's `margin: 14px 0 6px` shorthand may not reset
  the UA's logical margin-block-start/end on every engine.

Adds scripts/css_specificity_audit.py — the audit tool. It parses every
CSS file referenced from the project's three HTML entry points, computes
selector specificity (incl. :not/:is/:has math, attribute selectors, and
!important), and flags every place an unscoped legacy rule could outrank
a bare-class designed primitive. Honours per-page stylesheet manifests,
state-pseudo subset gating (a `:hover` rule overriding a resting-state
base rule is intentional, not a flip), and shorthand→longhand expansion
for font/padding/margin/border/background. Triage of remaining findings
(26 id-tier in default mode, 74 total at --all-tiers) confirmed all are
intentional designer overrides — id-scoped buttons, BEM modifier classes,
contextual ancestor selectors, last-child margin reset, [hidden] toggle.

* fix(css-audit): correct two cascade-resolution bugs flagged by Copilot

1. _parse_declarations dict insertion order didn't update on overwrite, so
   a sequence like `font-size: 13px; font: inherit; font-size: 12px;` would
   iterate as (font-size=12px, font=inherit) and the shorthand expansion
   then clobbered font-size back to `inherit` — wrong.  Delete-then-insert
   on overwrite so the last occurrence lands at the dict's tail and the
   shorthand expansion sees the real source order.

2. The cascade-winner tie-break used `rule.line_no` only, ignoring the
   stylesheet load order.  A rule at line 1000 of `base.css` looked
   "later" than a rule at line 50 of `style.css`, even though the page
   loads `base.css` BEFORE `style.css`.  Sort by `(file_index, line_no)`
   keyed off the element's per-page stylesheet manifest instead.
2026-04-27 20:05:25 -07:00
Patrick Buckley 7968f1b361 feat: auto-invalidate JWT and static assets on version upgrade (#307)
* feat: auto-invalidate JWT and static assets on version upgrade

Add a `ver` claim (major.minor) to user-facing JWTs so tokens from
previous versions are rejected after upgrade, triggering re-login.
Service tokens are excluded for rolling-deployment safety. Tokens
without a `ver` claim (pre-upgrade) are accepted for backward compat.

Inject `?v={__version__}` query strings into static asset URLs at
startup so browsers fetch fresh JS/CSS after any release. Vendored
libraries (KaTeX, Highlight.js, etc.) are skipped since they already
carry version numbers in directory paths. HTML responses now include
`Cache-Control: no-cache` to ensure browsers always revalidate.

Frontend detects upgrade-specific 401s and shows a contextual subtitle
("The server was updated — please sign in again"), then performs a full
page reload after re-auth to load the new versioned assets.

* refactor: address PR review — public API name, single decode, idempotent regex

Rename _version_slot() → jwt_version_slot() to make the cross-module
import explicit rather than relying on a private name.

Move version gating from validate_jwt() into check_request() via a new
AuthResult.token_version field. This eliminates the double JWT decode
that occurred on version-mismatch detection — the token is now decoded
once and the version compared afterward.

Guard version_html() regex against double-apply by excluding URLs that
already contain a query string ([^"?]+ instead of [^"]+).

* feat: structured version_mismatch code, ETag, cross-tab auth sync

Add structured "code": "version_mismatch" field to the 401 response
so the frontend detects upgrade-triggered re-auth without string
matching on the error message.

Add ETag headers to HTML index responses (server, console, and proxied
node UI). Combined with Cache-Control: no-cache, browsers send
conditional GETs and receive 304 between upgrades, saving bandwidth.

Add BroadcastChannel-based cross-tab auth sync so logging in on one
tab dismisses the login modal on all other tabs (and vice-versa for
logout).

Add a reminder to the vendored JS update script about the
version_html() regex lookahead.

* fix: remove unused import in test_web_helpers
2026-04-05 16:25:53 -07:00
Patrick Buckley db0baefeb2 feat: render rich media embeds for MCP tool results (#292)
* feat: render rich media embeds for MCP tool results

Detect structured media JSON (stream_url, results, sessions) in MCP
tool output and render interactive cards instead of plain text.

Web UI: media cards with thumbnail, title, metadata, and click-to-play
video/audio. HLS via lazy-loaded hls.js with direct-stream preference.
Collapsed raw JSON (API keys redacted) for inspection.

Discord: rich embeds with proxied thumbnail images (fetched by the bot
since Discord CDN cannot reach private media servers). Search results
as numbered lists, session state as "Now Playing" cards. Stream URLs
never exposed in embeds — web_url used for safe clickable links.

CI: vendor hls.js 1.6.15 with renovate tracking and update script.

* fix: address PR #292 review — SSRF guards, streaming fetch, tests

- URL validation: reject non-http(s) schemes and userinfo in thumbnail
  URLs. Private IPs intentionally allowed (media servers are on LAN).
- Streaming fetch: use http.stream() with aiter_bytes() and a running
  byte count to enforce the 2MB cap without buffering the full response.
  Validate content-type is image/* before downloading.
- Resilience: wrap try_build_media_embed in try/except in bot.py so a
  media embed failure falls through to the code-block path.
- LICENSE: download hls.js LICENSE from npm on update instead of only
  copying from old dir.
- Tests: add 19 new tests — try_parse_media (8 cases), _is_safe_image_url
  (7 cases), embed builders (4 cases including stream_url exclusion and
  string season/episode safety).

* chore: add LICENSE file for vendored hls.js

* fix: remove ANSI escape codes from tool preview fields

Preview text (tool args, URLs, queries) was wrapped in DIM/RESET ANSI
codes at the source in session.py, which leaked into SSE events and
rendered as raw escape sequences in Discord and the web UI.

Move ANSI styling to the CLI consumer (cli.py) where it belongs. Also
escape markdown in Discord tool name titles to prevent __ from being
interpreted as underline formatting.

* fix: drop [MCP: server] prefix from tool descriptions

The prefix made MCP tools look second-class compared to builtins,
causing models to hesitate using them. The server name is already
encoded in the tool name (mcp__server__tool).

* feat: pretty-print JSON tool output, player error state, broader key redaction

- JSON tool results are detected and pretty-printed with 2-space indent
  instead of rendering as a wall of text
- API key redaction extended to cover api_key, apiKey, api-key, and
  token query params across all tool output (not just media embeds)
- Video/audio player shows styled error message when stream fails to
  load instead of leaving a broken player element
- Both appendToolOutput and replayHistory use shared renderToolOutput()

* fix: designer review — player error retry, contrast, tool-cmd cap

- Player error: role="alert" for screen readers, retry button that
  reuses existing play handler, includes media title in error message
- Light theme: darken --red from #dc2626 to #b91c1c (5.7:1 contrast
  on --code-bg, was 4.3:1 failing WCAG AA at 12px)
- Pretty-print collapsed raw JSON in media embeds (was missed earlier)
- Cap .tool-cmd at 120px to prevent tools with many args from making
  approval blocks disproportionately tall in history replay
- Dedicated .media-player-error class instead of reusing .tool-output

* fix: Discord tool info name matching regression, suppress deprecation warning

The escape_markdown call on tool names was stored for matching against
ToolResultEvent.name, but event.name is raw/unescaped. The escaped name
never matched, so the "Running → Done" transition silently failed and
previews disappeared from the status embed.

Fix: store raw name for matching, use escaped name only for display.

Also suppress discord.py's re.sub count deprecation warning (Python
3.13+ issue, fixed upstream).

* fix: update MCP tool description tests to match prefix removal

* fix: address PR #292 review round 2

- Retry button: handle missing span children in click handler so retry
  buttons from player error state don't throw
- Footer count: use len(lines) instead of min(len(results), 10) to
  reflect actual rendered count after char budget truncation
- Null display: use "null" instead of "None" in JS tool arg preview
- Broader redaction: also redact JSON "api_key": "..." patterns
- SSRF hardening: block loopback and link-local IPs plus cloud metadata
  hostnames in thumbnail fetch (private LAN IPs still allowed)
2026-04-04 14:47:25 -07:00
Patrick Buckley 57080f4615 chore: release infrastructure for dual-track stable/experimental (#282)
* chore: release infrastructure for dual-track stable/experimental

CI/CD changes for the 1.0 release:

- Gate PyPI publish and Docker publish on CI success via workflow_run
- Add docker-publish.yml: builds and pushes to GHCR with smart tagging
  (stable gets :X.Y.Z/:X.Y/:stable/:latest, pre-release gets :experimental)
- Add stable/* and v* tags to CI and docker-scan triggers
- Remove stale [mq] extra and types-redis from CI (Redis MQ deleted)
- Remove stale redis from Renovate package rules

Release tooling:
- scripts/release.sh: bump version, uv lock, commit, tag (with --push)
- docs/releasing.md: documents stable/experimental workflow

Docker:
- Add /workspace mount point (WORKSPACE_MOUNT env var, defaults to empty volume)
- Update .env.example: remove stale Redis/auth-token refs, add workspace/model/discord

README:
- Remove beta warning, add hero image and release tracks table

* fix: derive release tag from git instead of workflow_run.head_branch

Use git tag --points-at HEAD after checkout to resolve the release
tag instead of relying on workflow_run.head_branch, which may not
reliably be the tag name for tag-triggered CI runs. Both publish
and docker-publish workflows now skip cleanly when no v* tag exists
at the checked-out commit.
2026-04-02 17:08:07 -07:00
Patrick Buckley c5d5d0b7cd fix: update-vendored-js.sh detects old version from filesystem
The script detected the old version from pyproject.toml, which Renovate
had already updated. This caused OLD_DIR == NEW_DIR, so the script
downloaded files then immediately deleted them.

Fix: detect old version from the actual directory on disk. Add a guard
that errors if old == new version to prevent silent data loss.

Also: run the fixed script to vendor katex 0.16.40 (fonts + css + js).
2026-03-23 13:03:49 -07:00
Patrick Buckley 22402e89de feat: add dependency management with Renovate, uv.lock, and security … (#83)
* feat: add dependency management with Renovate, uv.lock, and security scanning

Adds automated dependency update detection and vulnerability scanning
across all dependency layers (Python, vendored JS, TypeScript SDK, Docker,
GitHub Actions).

- Renovate config with 10 package groups and custom regex managers for
  vendored JS (KaTeX, Highlight.js, Mermaid) tracking via npm registry
- uv.lock for reproducible builds (80 packages)
- Dockerfile switched to uv sync --frozen with layer caching
- CI: pip-audit (via lock file), npm audit, lock-check jobs
- CI: lint job uses pre-commit for ruff version consistency
- Docker security scan workflow (weekly Trivy, HIGH/CRITICAL)
- Helper script for vendored JS library updates

* fix: resolve CI failures and address review feedback

- Update pre-commit hooks: ruff v0.9.10 -> v0.15.6 (fixes deprecated
  UP038 rule), mypy v1.14.1 -> v1.19.1
- Add per-file-ignore for N802 on sandbox.py (ast visitor convention)
- Fix pip-audit: install into uv venv so uv run can find it
- Pin uv-version in CI to match lock file generator (0.9.18)
- Upgrade vitest ^2.0 -> ^4.1 to fix esbuild GHSA-67mh-4wv8-2f99
- Vendored JS script: use grep -rl for auto-discovery of version refs
  (catches docs/architecture.md), fix LICENSE comment, portable grep
2026-03-15 15:46:48 -07:00