Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions .github/workflows/ci-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,17 @@ on:
# ship Node 24-compatible entrypoints — this just flips the runtime selector.
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
# 0.8.11 — onnxruntime-node's postinstall downloads CUDA GPU binaries from
# api.nuget.org on linux-x64: never used in CI (browser app → onnxruntime-web,
# CPU binaries bundled in the npm package) and api.nuget.org regularly times
# out on GitHub runners — THE recurring flake killing Security Scan /
# build-frontend at `pnpm install`. Documented knob of the package's install
# script; also set in frontend/Dockerfile + frontend/.npmrc.
ONNXRUNTIME_NODE_INSTALL: skip

jobs:
test-backend:
if: github.event.label.name == 'ci-test'
if: contains(github.event.pull_request.labels.*.name, 'ci-test')
runs-on: ubuntu-latest
defaults:
run:
Expand Down Expand Up @@ -115,12 +122,34 @@ jobs:
working-directory: frontend
run: pnpm build

# 0.8.11 — lint gates. eslint fails on ERRORS (0 today) and ratchets the
# WARNING budget so the react-19-strict backlog can only shrink (see
# docs/tech-debt/TD-20260509-react19-effect-rules.md). lint:i18n fails on
# a missing/undefined translation key (caught `common.saving` at wire-up).
- name: Lint — eslint (0 errors, warning budget)
working-directory: frontend
run: pnpm lint --max-warnings 162
- name: Lint — i18n key parity (fr/en/es)
working-directory: frontend
run: pnpm lint:i18n

- name: cargo check — desktop crate
working-directory: desktop/src-tauri
run: cargo check --locked
# --locked so a drift in Cargo.lock vs Cargo.toml also fails here,
# not just at release.

# 0.8.11 (C8) — typegen drift guard. Regenerate the per-type ts-rs
# bindings from the Rust models, then fail if a frontend-declared type in
# generated.ts is missing a field the Rust model has (the recurring
# "added a Rust field, forgot generated.ts" drift — hit 4× in 0.8.x).
- name: Types — ts-rs bindings + drift guard
# NOTE: this job's default working-directory is `backend` (see defaults
# at the top of the job) — paths below are relative to it.
run: |
cargo test export_bindings -- --nocapture
node ../frontend/scripts/check-types-drift.mjs

# ── Anti-regression lints ─────────────────────────────────────────
#
# These grep-based checks catch patterns that clippy can't:
Expand Down Expand Up @@ -223,7 +252,7 @@ jobs:
# as the AgentIo refactor removes the rust loop duplication. This is the
# mechanical guard that stops agents (and humans) from merging dup'd code.
duplication-check:
if: github.event.label.name == 'ci-test'
if: contains(github.event.pull_request.labels.*.name, 'ci-test')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -246,7 +275,7 @@ jobs:
# cache behaviour, missing-env handling, idempotency-collision guard
# on `source_session_id`. Stdlib only — zero dev deps, sub-second run.
test-python:
if: github.event.label.name == 'ci-test'
if: contains(github.event.pull_request.labels.*.name, 'ci-test')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -260,7 +289,7 @@ jobs:
run: python3 -m unittest discover -s backend/scripts -p 'test_*.py' -v

test-frontend:
if: github.event.label.name == 'ci-test'
if: contains(github.event.pull_request.labels.*.name, 'ci-test')
runs-on: ubuntu-latest
defaults:
run:
Expand Down Expand Up @@ -302,7 +331,7 @@ jobs:
# 0.6.0 "Create button silent", "JsonData ghost agent", "Quick APIs
# tab buttons leak" were all UI-only bugs that pure-JS tests can't
# see).
if: github.event.label.name == 'ci-test'
if: contains(github.event.pull_request.labels.*.name, 'ci-test')
runs-on: ubuntu-latest
# ── Playwright official container ─────────────────────────────────
# Chromium + chromium_headless_shell + ffmpeg + all OS deps are BAKED
Expand Down Expand Up @@ -454,7 +483,7 @@ jobs:
retention-days: 7

test-shell:
if: github.event.label.name == 'ci-test'
if: contains(github.event.pull_request.labels.*.name, 'ci-test')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -465,7 +494,7 @@ jobs:
run: make test-shell

security-scan:
if: github.event.label.name == 'ci-test'
if: contains(github.event.pull_request.labels.*.name, 'ci-test')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,44 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [0.8.11] - 2026-07-09

_« Socle clean » — the Phase-1 hardening batch from the 2026-07 full audit: security defaults, honest CI, operational reliability, and the two structural taxes (typegen, step dispatch) paid before the continual-learning work starts._

### Security

- **Secure network defaults — no more accidental LAN exposure.** A stock `docker compose up` now publishes Kronn on `127.0.0.1` only (gateway + frontend), so a fresh install is not reachable from other machines. Exposing to the LAN is an explicit opt-in (`KRONN_BIND=0.0.0.0`) — and when you opt in, the backend **refuses to start unauthenticated**: it requires `KRONN_AUTH_TOKEN`, enabled auth, or an explicit `KRONN_ALLOW_INSECURE_LAN=1` acknowledgment. Closes the "any LAN peer could trigger an Exec step with your credentials (→ host root via the mounted docker socket)" chain flagged by the audit, while keeping the deliberate WSL-backend / Mac-frontend split working (opt-in + token).

### Fixed

- **CI can no longer pass by skipping.** The `ci-test` label gate used `github.event.label.name`, which is empty on `synchronize` (a push to the PR) — so every push after the label was applied skipped ALL jobs and the PR stayed green untested. Now gated on `contains(github.event.pull_request.labels.*.name, 'ci-test')`: once labelled, every subsequent push re-runs the full suite.
- **No more env-race flakes in the MCP scanner / host-discovery tests.** Eight tests mutate process-global env (`KRONN_HOST_HOME`, `KRONN_TEMPLATES_DIR`) and could clobber each other under the parallel test runner; they're now `#[serial]`. `cargo audit` ignores are documented with a rationale and a review-by date (deferral, not a permanent waiver).
- **MCP sidecar authenticates to the backend (no more silent 401 cross-machine).** When the backend has an API token configured, it now exports `KRONN_AUTH_TOKEN` into the process env; the `kronn-internal` sidecar inherits it and sends `Authorization: Bearer` on every call. Previously an auth-enabled or LAN-exposed instance (the WSL-backend / Mac-frontend split) returned a silent 401 to its own sidecar. Locked by a sidecar contract test.

### Added

- **Failure notifications for autonomous runs.** Set `server.failure_notify_url` (or `KRONN_FAILURE_NOTIFY_URL`) to a Slack/Teams/generic webhook and any scheduled or auto-triggered run that ends Failed / Interrupted / StoppedByGuard POSTs an alert — a cron that dies at 6am surfaces immediately instead of waiting to be noticed. Best-effort: a dead webhook never affects the run.
- **Scheduled DB backups, optionally outside the data volume.** A periodic backup (default every 24h, keeps the last 7) runs automatically. Point `KRONN_BACKUP_DIR` at a host-mounted directory (compose has a commented bind-mount) so a lost Docker volume no longer takes the backups with it; `KRONN_BACKUP_INTERVAL_HOURS=0` disables it. The in-volume default is kept but logged as a warning.
- **Runs interrupted by a backend restart are marked `Interrupted`, not left "Running" forever.** A run in flight when the process dies (crash, container restart, `kill -9`, cargo-watch reload) is reconciled at boot to a new terminal `Interrupted` status (neutral grey, distinct from a red `Failed`) — so the active-runs badge clears, and a cron's "did the last run succeed?" check no longer sees a zombie as in-progress or as a failure.

### Changed

- **Ollama context window is now model-aware — zero configuration.** The num_ctx cap was a fixed 8192, silently truncating any large prompt (a 40 KB review diff) unless the operator knew to set an env var — which no regular user ever would. Kronn now asks Ollama for the model's own trained context (`/api/show`, cached per model) and caps there, bounded by a RAM-safe 32K ceiling (the KV-cache CPU-spill cliff). `KRONN_OLLAMA_NUM_CTX_CAP` remains as an expert override that wins over everything, and a likely-truncation now logs a WARN instead of being silent. num_ctx stays auto-sized to the prompt, so memory only grows when a step actually needs it.
- **Ollama steps get compact skill injection (num_ctx budget).** Skills attached to an Ollama step were injected in FULL (~500-800+ tokens each — a real 6-skill review step measured ~6k tokens = 74% of the default 8192 num_ctx, crowding out the actual task context). Ollama now joins the compact-injection list (Codex/Kiro/Vibe): ~150-char summaries keep the pointer without the cost. Steps that need full rules should inline them in the prompt template (the N2 review template does exactly that).
- **THE recurring CI flake (onnxruntime-node CUDA download) is dead — root cause found.** The original fix (`package.json#pnpm.neverBuiltDependencies`) was silently ignored after the pnpm 11 upgrade (the field moved), so `onnxruntime-node`'s postinstall went back to downloading CUDA GPU binaries from api.nuget.org on linux-x64 — never used (browser app → onnxruntime-web; CPU binaries are bundled) and timing out on GitHub runners, killing Security Scan and build-frontend at `pnpm install`. Now skipped via the package's documented `ONNXRUNTIME_NODE_INSTALL=skip` knob (CI workflow env + frontend Dockerfile + .npmrc); the dead `pnpm` field is removed from package.json (constant warning noise). Also fixed the typegen drift-guard CI step running `cd backend` from inside `backend/` (the job's default working-directory).
- **A disabled workflow now explains itself instead of sitting mute.** Clicking "Lancer" on a disabled workflow (clones land disabled by design) used to do nothing, with zero feedback. The launch button now carries an explanatory tooltip (card + detail), the detail shows a "⏸ Désactivé" chip, and a one-click "Activer" button enables the workflow on the spot (with a success toast). The launch-variables modal flow (popup with fields, required-var blocking, trigger with values) is now pinned by tests — it had none.
- **Per-step model tier, selectable in the wizard.** An Agent step now has a tier picker (⚡ economy / 🎯 default / 🧠 reasoning) next to the agent — so one workflow can mix models (e.g. a cheap 8b filter step + a 32b reasoning gate for a local 2-stage review). The chosen tier shows as a small badge on the step in the workflow detail + compact pipeline.
- **Branch map: per-jump colours (CVD-validated), hover-to-isolate, fanned crossings.** The #15 branch map coloured every Goto the same; now each jump gets a distinct hue from a colourblind-safe categorical palette (validated per theme — dark uses Kronn's brand hues, light a distinct passing set), backward loops stay dashed (colour + shape, never colour alone), overlapping arcs fan out geometrically, and hovering the map dims all arcs but the one under the cursor so a crossing line is easy to follow.
- **Typed error codes on API responses (incremental).** `ApiResponse` gained an optional `error_code` (`not_found` / `validation` / `conflict` / `internal`) so the frontend and the MCP tools can branch on the KIND of failure instead of string-matching the message. Back-compatible (omitted from the wire when unset; legacy `err()` untouched); adopted first across the workflows handlers (not-found cases). Groundwork for the headless MCP surface.
- **UI language follows the browser on first load.** `getUILocale()` no longer hardcodes French — with no saved preference it detects `navigator.languages` (fr/en/es, else English), so the ~95% of the market that isn't francophone doesn't hit a French UI out of the box. The onboarding tour's section labels are now translated too (they were the last hardcoded-French strings; titles/bodies were already localized).
- **Step-type schema can't silently drift between Rust and the MCP sidecar.** The Python sidecar hand-lists the workflow step types (`step_types_closed_set`, read by agents to author workflows) with no compile link to Rust. A new guard test fails if that list drifts from the `StepType` enum. (The Rust runner's own dispatch is already exhaustive-`match`-checked at every site, so the compiler prevents forgetting a variant in the main/rollback/validation paths — the class of the 0.8.6 #59 incident; a full `StepExecutor` trait to de-duplicate those sites is deferred as an optional DRY refactor, not a correctness fix.)
- **Typegen drift is now caught in CI.** Adding a field to a Rust model but forgetting to mirror it in `frontend/src/types/generated.ts` was a silent, recurring bug (hit 4× in 0.8.x). A new field-level guard (`make typegen` + CI) regenerates the ts-rs bindings and fails if any frontend-declared type is missing a field the Rust model has. Back-filled the 32 fields that had already drifted across 10 types (e.g. `ServerConfig` carried only `host`+`port`, `WorkflowRun.state`, `WorkflowExportEnvelope.referenced_workflows`). See `TD-20260701` for the remaining full-regen migration (now guarded, no longer urgent).
- **Run history queries are bounded.** The unpaginated `GET /:id/runs` now caps at the 500 most recent runs (loading every row *with its full step results* on each page-open was the growth risk on a fast cron); the UI already folds + paginates. Optional retention (`server.run_retention_days`, default 0 = off) purges terminal runs older than N days at boot while preserving parent runs still referenced by a retained child — bounding the run table (76% of the DB) without silently dropping history unless you opt in.
- **Frontend lint gates are now enforced in CI.** `eslint` (0 errors, with a ratcheted warning budget so the React-19-strict backlog can only shrink) and `lint:i18n` (fails on a missing/undefined translation key — it caught `common.saving`, now added in all three locales) run on every CI push. Fixed the 31 pre-existing eslint errors (mostly useless-escapes in the i18n JSON examples; plus one real conditional-hook bug in `MarkdownContent`).
- **A vitest guard fails the build on phantom CSS variables.** Any `var(--kr-…)` used without a fallback and not defined in `tokens.css` now fails a test. Defined the ~20 phantom tokens the audit found (aliased to canonical tokens so they adapt across all themes; RGB triples for `rgba()` overlays in both dark and light). This is the class of bug behind the earlier black-on-dark contrast issue.

---

## [0.8.10] - 2026-07-02

### Security
Expand Down
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,14 @@ check:

## Generate TypeScript types from Rust models
typegen:
@echo "$(GREEN)▸ Generating TypeScript types...$(RESET)"
@echo "$(GREEN)▸ Generating ts-rs bindings from Rust models...$(RESET)"
cd backend && cargo test export_bindings -- --nocapture
@echo "$(GREEN)▸ Types written to frontend/src/types/generated.ts$(RESET)"
@echo "$(GREEN)▸ Checking generated.ts for field drift vs the Rust models...$(RESET)"
@node frontend/scripts/check-types-drift.mjs || { \
echo "$(YELLOW)▸ generated.ts is missing fields — add them (see backend/bindings/<Name>.ts),$(RESET)"; \
echo "$(YELLOW) or run 'node frontend/scripts/assemble-generated-types.mjs' for a full regen.$(RESET)"; \
exit 1; }
@echo "$(GREEN)▸ generated.ts is in sync with the Rust models.$(RESET)"

## Run backend tests (skips ts-rs exports to avoid overwriting generated.ts)
test-backend:
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.8.10
0.8.11
8 changes: 8 additions & 0 deletions backend/.cargo/audit.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
#
# TODO: drop these ignores once calamine releases a build on quick-xml >=0.41
# (track: https://github.com/tafia/calamine/issues).
#
# POLICY (0.8.11): every ignore below is DATED and must be re-reviewed at the
# review-by date — an ignore is a deferral, not a permanent waiver. At review:
# check whether calamine ships on quick-xml >=0.41 (`cargo update -p calamine`),
# and if so remove the entry. Do not add an ignore without a rationale + a
# review-by date. CI runs `cargo audit` (see .github/workflows/ci-test.yml).
#
# RUSTSEC-2026-0194 / -0195 — added 2026-06-30, review by 2026-09-30.
[advisories]
ignore = [
"RUSTSEC-2026-0194",
Expand Down
2 changes: 1 addition & 1 deletion backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "kronn"
version = "0.8.10"
version = "0.8.11"
edition = "2021"
description = "Self-hosted AI dev workflow control plane"
license = "AGPL-3.0-only"
Expand Down
46 changes: 46 additions & 0 deletions backend/scripts/test_disc_introspection_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -3472,5 +3472,51 @@ def test_limit_caps_results(self):
self.assertEqual(out["disc_count"], 3)


class HttpAuthHeaderTests(unittest.TestCase):
"""0.8.11 — the sidecar authenticates to the backend: `_http`/`_http_text`
send `Authorization: Bearer <token>` iff KRONN_AUTH_TOKEN is set. This is the
contract that makes an auth-enabled / LAN-exposed backend reachable by its
own sidecar (was a silent 401 before the boot injects the token)."""

def setUp(self):
self.mod = _load_module()

def _ok_response(self):
cm = mock.MagicMock()
cm.__enter__.return_value.read.return_value = b'{"success": true, "data": {}}'
cm.__exit__.return_value = False
return cm

def test_http_adds_bearer_when_token_present(self):
with mock.patch.dict(os.environ,
{"KRONN_BACKEND_URL": "http://127.0.0.1:3140", "KRONN_AUTH_TOKEN": "sekret"},
clear=True), \
mock.patch("urllib.request.urlopen", return_value=self._ok_response()) as urlopen:
self.mod._http("GET", "/api/health")
req = urlopen.call_args.args[0]
self.assertEqual(req.get_header("Authorization"), "Bearer sekret")

def test_http_omits_auth_when_no_token(self):
with mock.patch.dict(os.environ,
{"KRONN_BACKEND_URL": "http://127.0.0.1:3140"},
clear=True), \
mock.patch("urllib.request.urlopen", return_value=self._ok_response()) as urlopen:
self.mod._http("GET", "/api/health")
req = urlopen.call_args.args[0]
self.assertIsNone(req.get_header("Authorization"))

def test_http_text_adds_bearer_when_token_present(self):
cm = mock.MagicMock()
cm.__enter__.return_value.read.return_value = b'{"kind":"kronn.workflow"}'
cm.__exit__.return_value = False
with mock.patch.dict(os.environ,
{"KRONN_BACKEND_URL": "http://127.0.0.1:3140", "KRONN_AUTH_TOKEN": "tok2"},
clear=True), \
mock.patch("urllib.request.urlopen", return_value=cm) as urlopen:
self.mod._http_text("GET", "/api/workflows/x/export")
req = urlopen.call_args.args[0]
self.assertEqual(req.get_header("Authorization"), "Bearer tok2")


if __name__ == "__main__":
unittest.main()
Loading
Loading