Blocking multi-scanner stack: CodeQL security-extended, Semgrep, ruff, npm audit — every finding fixed, gated through the existing gates - #1246
Merged
Conversation
added 3 commits
August 22, 2026 11:41
…orks
Default setup was switched off, so nothing was being scanned at all. It had to
go because its Go analysis could not be repaired from anywhere: it installed the
Go its extractor was built with (1.26.6) and pinned GOTOOLCHAIN=local, so
against a go.mod requiring 1.27 it could neither build nor fetch a usable
toolchain:
go: go.mod requires go >= 1.27.0 (running go 1.26.6; GOTOOLCHAIN=local)
Failed to run `go mod tidy -e` in .
Extraction failed for all discovered Go projects.
Every main-targeting PR carried that failure from the Go 1.27 bump (#1240,
promoted in #1242) on, so the repo's Go code went unscanned for that whole
stretch. Default setup is zero-config and exposes no Go version input, hence
advanced setup.
.github/workflows/codeql.yml restores both analyses default setup ran —
security over go, python, javascript-typescript and actions; code quality over
go, python and javascript-typescript — and gives Go an interpreter via
actions/setup-go with go-version-file: go.mod, so the version keeps one
declaration point instead of a copy that goes stale. The three languages in
both sets pass analysis-kinds: code-scanning,code-quality, building one
database and running both suites over it rather than extracting twice.
Triggers are push on main and pull_request on main and dev. Covering dev PRs is
the one place this exceeds default setup, which never ran on them: every change
lands on dev first and main only receives promote merges, so scanning main alone
surfaces a finding for the first time on a promote commit. A weekly Monday 10:00
UTC cron is offset from the three existing scheduled lanes.
dev-ci.yml's header listed CodeQL among the checks deferred to the dev->main
gate. That is no longer true, so it now says where CodeQL runs instead.
The three existing upload-sarif calls are untouched.
Signed-off-by: Brad Flaugher <brad@elcanotek.com>
… tag
Two defects in the first cut, both found by reading the run log rather than the
check mark — the first run was green with neither working.
1. `analysis-kinds: code-scanning,code-quality` does not do what it looks like.
The action logged two ##[error] lines and then exited 0:
The `analysis-kinds` input is experimental and for GitHub-internal use
only. [...] An analysis kind other than `code-scanning` was specified in
a custom workflow. This is not supported and will become a fatal error
in a future version of the CodeQL Action. If your intention is to use
quality queries outside of Code Quality, use the `queries` input with
`code-quality` instead.
[...] Specifying multiple values as input is no longer supported.
Continuing with only `analysis-kinds: code-scanning`.
Confirmed in the artifacts, not just the warning: the Go job loaded only
codeql/go-queries and uploaded a single go.sarif, so the code-quality half of
the coverage this change claims to restore was not running at all. Switched
to `queries: code-quality` as the message directs. Code quality as a distinct
analysis KIND stays closed to custom workflows; the quality queries
themselves now run, surfacing as ordinary code-scanning alerts.
2. Go extraction had exactly one hole, and it was the worst possible file. The
extractor reported 426 files against 427 non-test .go files in the tree; the
missing one was internal/sandbox/host.go, the unsandboxed host executor,
fenced behind `//go:build fleet_host_executor` and therefore absent from the
default build. ci.yml and dev-ci.yml both pass that tag to `go vet` and
`go test` precisely so it is not unchecked. GOFLAGS on the autobuild step
passes it here for the same reason.
Also recorded in the file's header: GOTOOLCHAIN=local is set by the codeql-action
itself, not by the generated default-setup workflow as was assumed. The fix works
because setup-go makes the LOCAL toolchain 1.27.0, satisfying go.mod, not because
the pin is gone.
Signed-off-by: Brad Flaugher <brad@elcanotek.com>
docs/CODEQL.md records what shipped, what deviated from a like-for-like restoration of default setup, and what was deliberately left out — with the verified/assumed split stated explicitly, since the last two bugs in this area both shipped because a toolchain layout was assumed rather than observed. Verified and quoted from the run's own log archive: extraction succeeded for both discovered Go projects, 916 packages, 426 .go files including host.go, and distinct queries evaluated per language across the two runs (72->116 go, 90->292 python, 178->374 javascript-typescript, 36->36 actions). Also corrected there: GOTOOLCHAIN=local is set by the codeql-action itself, in four steps of our own Go job, not by the generated default-setup workflow. The fix works because setup-go makes the local toolchain 1.27.0 — "give `local` something good enough", not "unset the pin". Stated as NOT verified: no push-on-main or scheduled run has executed yet; test files stay outside the database (unchanged from default setup, not a regression); the lines-of-code metric value is never printed to the log, so no line count is claimed; and build-mode: manual was not built because autobuild works. CHANGELOG entry and an AGENTS.md "Where to look" pointer alongside. Signed-off-by: Brad Flaugher <brad@elcanotek.com>
bradflaugher
marked this pull request as ready for review
August 22, 2026 12:00
added 5 commits
August 22, 2026 12:29
CodeQL still is not blocking, and this commit does not make it blocking: `ci-gate` remains the only required status check on main, and requiring a check is a repo-settings action that a workflow file deliberately cannot perform. What it fixes is the shape of that future decision. CodeQL cannot be folded into ci.yml's `CI gate` at all — `needs` cannot reach across workflow files — so without this, making CodeQL required would mean naming `Analyze (go)`, `Analyze (python)`, `Analyze (javascript-typescript)` and `Analyze (actions)` individually in branch protection. That has to be re-pointed by hand every time the matrix gains or loses a language, and both ways of getting it wrong are bad: a required check that never reports again blocks every PR, and a removed one silently stops gating. One aggregate job has neither failure mode, and it is the pattern this repo already uses twice — ci.yml's `CI gate` and dev-ci.yml's `Dev gate`, including the same `if: always()` + join(needs.*.result) shape. `needs: [analyze]` on a matrix job collapses to a single aggregate result, so with fail-fast: false every language still runs and reports before the gate evaluates them. docs/CODEQL.md now also records where findings actually surface, since an empty Security tab is easy to misread as a broken pipeline: the alert list shown there is the DEFAULT BRANCH's, and this workflow's only push trigger is main, so it repopulates at a dev->main promotion rather than when a PR is scanned. PR runs report on the PR, where CodeQL additionally suppresses file-coverage detail. Also noted: the quality queries this change enables (+44 go, +202 python, +196 javascript-typescript) have never run against this codebase, so any pre-existing finding becomes an alert on merge — a reason to add `CodeQL gate` to the ruleset after a few green promotions rather than on day one. Signed-off-by: Brad Flaugher <brad@elcanotek.com>
Two gaps this closes, both surfaced by the question "can you see scanning results from the logs, and can you gate on this?" 1. A CodeQL run reports NOTHING about what it found to its own log. It writes SARIF, uploads it, and exits 0 — with findings or without. Grepping a full run's log archive for any alert or result count returns nothing; the only result-shaped lines are `Exporting results to SARIF...` and `Successfully uploaded results`, which say a file moved, not what was in it. That leaves a run's real outcome invisible to anyone reading CI output, to `gh run view`, and to any automation holding the log but not the code-scanning API. The analyze step now also writes SARIF locally via `output:` (results are still uploaded — `upload` defaults true) and a following step jq-summarizes per-rule counts into both the job log and the step summary. This is the pattern govulncheck-scheduled.yml already uses on its own SARIF. It is reporting only and never fails the job. When no SARIF was written it says so instead of printing "No findings.", because reporting a clean result you did not observe is the mistake this repo keeps recording. The jq was exercised against SARIF fixtures before pushing: findings spread over two files, a repeated ruleId, a result with no `level` key (falls back to note), an empty `results` array, a run with no `results` key, and a doc with no `runs` key. The last four all yield "No findings." rather than a jq error. 2. docs/CODEQL.md now states the distinction that makes "gate on CodeQL" ambiguous: a required status check on the job gates on the analysis having RUN, not on what it FOUND. A CodeQL job with a hundred open alerts still exits 0 and reports green — which is both why the toolchain break hid behind a red-but-not-required check, and why a green check is not evidence of a clean tree. Blocking on findings is a separate feature, code scanning merge protection (ruleset -> Code scanning rule -> tool CodeQL -> severity thresholds), which is available here at no cost because this repo is public. Signed-off-by: Brad Flaugher <brad@elcanotek.com>
…ep owns Actions
Reshapes the scanning stack on measurements rather than on "more scanners is
better". docs/SCANNING.md is the full note; docs/CODEQL.md carries the CodeQL
half.
ruff is new, and it BLOCKS (ci.yml + dev-ci.yml `python` job, both wired into
their gate jobs). fleet ships 13 Python files — the sandbox FileOp helper, the
python bridge, the bento-slides and data-profiler skill scripts, MCP test
servers — and nothing linted any of them: Go had golangci-lint, the web tier had
oxlint, Python had neither. Rule selection is narrow and ruff.toml records the
numbers behind that: default rules find 3 findings here, a broad selection finds
333, of which 176 are %-format style, 43 magic values, 35 line length. Gating on
that would be a whole-tree reformat for no correctness gain.
Three real findings were fixed so the gate is clean on day one and a new
violation is a regression rather than backlog noise: an unused import in
dummy_server.py, a lambda assignment in bento_pdf.py, and a byte-identical
duplicate `has_guard` definition in bento_doc.py whose second copy silently
shadowed the first. `ruff format` is reported but NOT gated — the tree has never
been ruff-formatted, so failing on it would block every PR on a reformat nobody
scheduled.
CodeQL narrows to security queries only. The code-quality suite was enabled,
measured, and dropped: 32 findings, every one note-level, ZERO security
findings. For Go and the web tier it duplicates golangci-lint (gosec,
staticcheck, revive, unparam, gocritic) and oxlint, which already block; 28 of
the 32 were Python, which is ruff's job now and done in a second with autofix
instead of ~40s without; and 3 were false positives on correct code
(`value != value`, the idiomatic NaN test). What CodeQL keeps is the thing
nothing else here can do — interprocedural taint, which is the actual shape of
"a credential must not reach a log sink, the model context, or the sandbox".
Semgrep is new, scoped, and advisory. Pointing it at p/golang, p/javascript and
p/python was tried and rejected on evidence: 55 findings, and all 6 non-Actions
findings were false positives. tls.go's open-redirect is an HTTP->HTTPS upgrade
to the same host; runner.go's math/rand is jitter; elcano.go's cookie is a
deletion cookie with no secret; httptool.go's interface{} is required because the
value feeds a jq program; proxy.ts's X-Frame-Options value is the literal
"DENY"; and fileops.py's advice — 0o644 for a sandbox directory — would be a
security REGRESSION if followed. Three of the six were already formally triaged
and suppressed for gosec, which already blocks. Re-reporting adjudicated
findings is how a scanner teaches people to ignore it.
What ships instead is p/github-actions, which found 51 instances of one real
issue nothing else in this repo checks: actions pinned to a mutable tag rather
than an immutable commit SHA, which runs attacker-controlled code with this
repo's token if a tag moves. It is advisory because all 51 are real and
repinning every workflow is its own PR — failing CI for an unscheduled backlog
just trains people to ignore the lane. Flip continue-on-error off in the PR that
repins.
Both scanners now print a per-rule summary to the job log and the step summary,
and Semgrep uploads its raw JSON as an artifact for a fixing agent to consume.
Supporting changes: RUFF_VERSION is duplicated across ci.yml and dev-ci.yml, so
scripts/check_versions_test.go now asserts the two agree (mutation-tested: the
assertion fails when the pins diverge). `make lint` gains lint-python, which
skips LOUDLY with the install command when ruff is absent rather than quietly
doing nothing. dev-ci.yml's header and AGENTS.md's build/CI prose now name the
Python lane, and AGENTS.md points at docs/SCANNING.md.
Gate: make build, make lint (0 issues + ruff clean), make test (exit 0),
make lint-migrations. All Python files still byte-compile and the bento golden
tests pass.
Signed-off-by: Brad Flaugher <brad@elcanotek.com>
The scanning stack now passes clean and fails on anything new. Turning a gate on over an unfixed backlog is how a gate becomes something people route around, so every finding was fixed or adjudicated before the switch was flipped. All 53 action references pinned to commit SHAs. Semgrep's github-actions-mutable-action-tag found 51 instances of actions referenced by a mutable tag (actions/checkout@v7); if such a tag moves, attacker-controlled code runs with this repo's GITHUB_TOKEN. Every `uses:` across all 12 workflows is now @<40-hex> with the version in a trailing comment — the form Dependabot reads and updates, and .github/dependabot.yml already watches the github-actions ecosystem. Each SHA is the commit the previously-used tag resolved to at pin time, so the pin is behaviourally identical to the runs already verified green; a pin should not smuggle in a version bump. The only `uses:` lines left on @main are two inside COMMENTS, documenting how a downstream bundle repo calls fleet's reusable workflows — @main is correct guidance there, and Semgrep does not flag them because a YAML comment is not a `uses:` key. Semgrep now blocks over all four packs (p/github-actions, p/golang, p/javascript, p/python) with --error and no continue-on-error. The 6 false positives are suppressed at the line with `nosemgrep: <rule-id>` plus a stated reason, scoped to the rule so a different rule on the same line still reports. Three of the six were already formally triaged and suppressed for gosec, which runs inside golangci-lint and already blocks; one of them — advising 0o644, world-readable, for a sandbox directory — would have been a security REGRESSION if followed. Every suppression was mutation-tested: strip it and the finding reappears, keep it and the finding is gone. That check matters because "0 findings" has two explanations — the waivers work, or the rules silently stopped matching — and only one is safety. Verified across all three comment syntaxes. CodeQL now fails on findings. Previously the analyze step exited 0 whether it found nothing or a hundred alerts, so a red check could only ever mean "the scanner broke" — which is precisely how the Go toolchain break sat unnoticed for weeks behind a red-but-not-required check. Threshold is ANY finding, which is safe because the security suite reports zero across go, python, javascript-typescript and actions. The step also fails when no SARIF was written at all, rather than reporting a clean scan that never happened. Both scanners report as their own checks (CodeQL gate, Semgrep scan) rather than through ci-gate, because a job's `needs` cannot reach across workflow files. Making a red check BLOCK a merge still requires adding those checks to the branch ruleset; a workflow file cannot make itself required. Two knock-on defects found and fixed while doing this: - SHA pinning broke two regexes in scripts/check_versions_test.go that matched `golangci-lint-action@v\d+`. Those assertions fail OPEN — a non-match logs "skipping" rather than failing — so the pin would have silently disabled the golangci-lint version agreement checks. Widened to tolerate a pinned ref plus its trailing version comment, then mutation-tested against the real docs text to confirm they still bite rather than skip. - A standalone nosemgrep comment inside a Go import block makes goimports reformat the group, failing lint. That waiver is a trailing comment on the import line instead; re-tested to confirm the trailing form still suppresses. Deliberately NOT included, with measurements, in docs/SCANNING.md: `ruff format` (9 of 13 files differ, a 3725-line diff — cosmetics, and landing it here would bury the security change) and widening ruff's rule set (`--select B,SIM,S` adds 21 findings, of which the interesting ones are 2x B905 zip-without-strict and 1x SIM115 open-without-context-manager). Gate: gofmt clean, make build, make lint (0 issues + ruff clean), make test (exit 0), make lint-migrations, and semgrep --error over all four packs at 0 findings. Signed-off-by: Brad Flaugher <brad@elcanotek.com>
"No findings." on its own is indistinguishable from "scanned nothing", which is the green-but-vacuous outcome this whole stack exists to rule out. Verifying the first blocking run required downloading the artifact and inspecting the JSON by hand — the log said the scan was clean but not what it had looked at. That is an instrumentation gap, and it is the same class of gap as CodeQL not printing its own findings. Both summary steps now print coverage alongside the verdict: - Semgrep: files scanned, files skipped, and a per-extension breakdown, so it is visible at a glance that every pack actually applied to its language rather than one silently matching nothing. Parse/scan errors are listed with their paths instead of only being counted, since an error means a rule or file did not fully run. - CodeQL: the number of files in the database, read from the source archive the database was built from. Note CODEQL_DB is not always the matrix language — the extractor names the javascript-typescript database "javascript". Both jq blocks were dry-run against the real semgrep.json artifact from run 32575445870 before being committed, which is where these numbers come from: 898 files scanned, 0 skipped, 427 .go / 298 .ts / 134 .tsx / 13 .py / 22 yml+yaml, 3 warn-level parse errors in 2 files. Signed-off-by: Brad Flaugher <brad@elcanotek.com>
added 7 commits
August 22, 2026 13:35
The new coverage line interpolated `${{ runner.temp }}` directly into a bash
`run:` script. That is the exact shape semgrep's gha-curl-pipe-shell and
curl-eval rules exist to flag — a GitHub expression expanded into a shell script
before the shell ever sees it — and it also breaks their bash sub-parser, so
those two rules stopped evaluating against codeql.yml entirely.
Caught by the coverage reporting added in the previous commit: parse/scan errors
in the Semgrep summary went from 3 to 5, with .github/workflows/codeql.yml newly
among them. Without that instrumentation the lane would have stayed green while
quietly analyzing this file with two fewer rules — the same green-but-vacuous
failure the whole change is built to prevent, introduced by the change itself.
$RUNNER_TEMP is the equivalent env var, parses cleanly, and avoids the
interpolation entirely. Verified: codeql.yml now reports 0 errors and 0 findings
under p/github-actions, and repo-wide errors are back to the 3 pre-existing ones
(a bash snippet in build-sandbox-image.yml, a TS type in fixtures.ts).
The remaining `${{ runner.temp }}` references are in `with:` and `env:` blocks,
which is the correct placement — the value never reaches a shell unparsed.
Signed-off-by: Brad Flaugher <brad@elcanotek.com>
Mechanical, behaviour-free reformat of 9 files (~3.7k diff lines), kept as its own commit so the substantive changes around it stay reviewable. Validated before the gate flipped: `ruff check` still clean, every file byte-compiles, the full Go suite passes (the bento/fileops golden tests exercise these scripts), and the fileops.py line-level nosemgrep waiver survived the reformat (re-scanned: 0 python findings). The `ruff format --check` gate lands in the next commit; this commit is what makes that gate start clean instead of red. Signed-off-by: Brad Flaugher <brad@elcanotek.com>
…ormatting Closes the three things still standing between "the scanners run" and "the scanners are load-bearing", plus every finding doing so surfaced. Nothing is deferred. GATING, CORRECTED. The earlier design note claimed making CodeQL/Semgrep merge-blocking needed a branch-protection click, reasoning from "`needs` cannot cross workflow files". Incomplete: codeql.yml and semgrep.yml are now REUSABLE workflows (`on: workflow_call`) that ci.yml and dev-ci.yml call as jobs, and a job that calls a reusable workflow sits in a gate's `needs` like any other job. So `CI gate` — already the single required check on main — and `Dev gate` now block on scanner findings with no settings change anywhere. Their own push/pull_request triggers are removed so nothing runs twice; each keeps its weekly re-scan cron (new queries/rules against unchanged code) plus a workflow_dispatch. In ci.yml the calls are docs-only-skippable like the other heavy jobs (a docs-only change cannot touch scanned code, and the gate treats a skip as a pass); in dev-ci they are unconditional because Dev gate demands strict success and the fast lane has no docs-only detection — which also means direct pushes to dev get scanned. NPM AUDIT, NEW BLOCKING GATE. `npm audit --audit-level=low` runs in both web jobs, lockfile-only, before the expensive `npm ci`, failing on any severity — the npm counterpart of the govulncheck gate, clock-dependent by design. web/ was already clean (0 vulns). scripts/rampart-service HAD NO LOCKFILE AT ALL, so nothing could audit it and installs were unreproducible; generating one exposed 5 high-severity vulnerabilities it had been hiding: sharp <0.35.0 (libvips CVE-2026-33327/-33328/-35590/-35591) and adm-zip <0.6.0 (GHSA-xcpc-8h2w-3j85) via onnxruntime-node. No upstream release fixes either — the latest @huggingface/transformers still pins sharp ^0.34.5, and npm's suggested "fix" was a BREAKING DOWNGRADE of transformers to 3.8.1 — so package.json carries two overrides (sharp ^0.35.3, adm-zip ^0.6.0; each the release immediately after its vulnerable line). The overridden stack was installed and load-tested, not just resolved: sharp renders a PNG through the new libvips, transformers loads on it, rampart exports its API, adm-zip 0.6 round-trips a zip. Both trees now audit at 0. Drop the overrides when upstream ships fixed ranges. RUFF FORMAT, NOW A GATE. `ruff format --check` blocks in both CI python jobs and in `make lint` (lint-python). Safe because the previous commit formatted the whole tree, so the gate starts clean and a failure means one new file. ALL THREE SEMGREP PARSE ERRORS FIXED, so no file is partially covered — a partial parse silently drops rules from a file, which is coverage loss wearing a green check: - build-sandbox-image.yml interpolated ${{ steps.build.outcome }} into its run: script; now passed via env (also the script-injection-safe form — the same fix codeql.yml got for $RUNNER_TEMP). - The same script's ${tag:-(tag unavailable)} expansion default is valid bash, but the bare paren chokes semgrep's bash sub-parser; hoisted to a plain `if [ -z "$tag" ]` assignment. - fixtures.ts used an inline `import("@playwright/test")` type; now a named `import type { BrowserContext }`. Validated with the real web toolchain: npm ci, oxlint, tsc --noEmit, and all 1104 vitest tests pass. Final measured state, whole tree: semgrep --error over all four packs exits 0 with 0 findings, 898 files scanned, 0 parse errors; both npm trees 0 vulnerabilities; ruff check + format --check clean; gofmt clean; make build, make lint, make test (exit 0), make lint-migrations all green. Signed-off-by: Brad Flaugher <brad@elcanotek.com>
…ed, grype High, canary, cron alarms Every item from the audit self-assessment that can live in code, closed. One item closed by a reasoned rejection rather than code, with the license cited. ruff B/SIM/S (bandit tier): all 21 measured findings fixed, families ENABLED. Both zip() sites get strict=True — each provably equal-length (bento_pdf appends pages and contents in lockstep in the same loop; profile.py sits behind an explicit len(row) != len(cols) guard) — so a future desync fails loud instead of silently truncating a PDF or a profile. bento_doc's unclosed NamedTemporaryFile moved inside its with (tmp=None sentinel keeps the unlink-on-failure path exact). The fourteen deliberate best-effort try/except-pass sites — kernel cleanup, the duck-typed pandas/numpy probes in normalize_json_value, unlink-on-failure in the sandbox fileops commit path — became explicit contextlib.suppress with the intent stated at each site; the suppressions are semantically identical, and the sandbox fileops conversion is covered by its test suite (full make test green). The one subprocess.Popen carries a reasoned `# noqa: S603`: argv is sys.executable plus literal flags plus a connection-file path this process just created with mkstemp — nothing model- or user-controlled. The waiver was mutation-tested: stripping the noqa re-fires S603. Two of my own first-cut mistakes fixed in the same pass: a prose comment beginning with the literal token "# noqa" (parsed as a malformed directive) and a nested-with that tripped SIM117. CodeQL: security-extended on all four languages. Adopted the way every other gate here was — the default suite measured zero findings, so the broader suite starts from a clean baseline, and this PR's own run (whose Fail-on-findings step reads the SARIF) is the measurement. Anything extended surfaces must be fixed or reasoned away; it cannot accrue. Grype: gate tightened from fixable-CRITICAL to fixable CRITICAL+HIGH, measured first: the published sandbox image (pinned grype 0.117.0, checksum-verified, scanned directly from GHCR) carries ZERO fixable Critical/High RPM findings — its only fixable findings are two Medium openssh advisories that the next routine image rebuild picks up. Policy mutation-tested three ways: the real scan passes, an injected fixable High fails, an injected fixable Medium still passes. scripts/check-npm-overrides.sh: an override is a fork of upstream's intent, correct only while upstream is broken — and the day upstream fixes itself, nothing notices, leaving Dependabot silently pinned down. Both CI lanes now ask the registry what floor @huggingface/transformers and onnxruntime-node declare, and FAIL with removal instructions once those reach sharp>=0.35 / adm-zip>=0.6. Registry flake is a skip with a notice, never a verdict (npm audit beside it is the CVE gate proper). Mutation-tested in both directions. Cron failure alarms: all four scheduled scan lanes (codeql, semgrep, govulncheck, grype) file a deduped GitHub issue when a SCHEDULED run fails — a red cron has no PR to surface it, which is exactly the rot pattern that let the CodeQL toolchain break sit red for weeks. In the workflow_call path the alarm job stays skipped (schedule-only condition), so callers never need to grant issues: write. Semgrep rule vendoring: investigated and REJECTED on license grounds. The Semgrep Rules License v1.0 permits "your own internal business purposes" only and states "This license does not allow you to distribute the rules" — committing the packs to this public MIT repo would be redistribution. The binary stays pinned; the rules stay registry-fetched with the failure mode named in docs/SCANNING.md. Docs: every stale "fixable CRITICAL" mention updated (AGENTS.md, TESTING.md, SANDBOX-IMAGE-FRESHNESS.md, CONTRIBUTING.md, ci.yml comment); SCANNING.md and CODEQL.md carry the new levels and the license decision; ruff.toml's header records why B/SIM/S went from measured-deferred to fixed-enabled and that PLR0124 stays rejected (its only hits are the idiomatic NaN test). Gate: yaml parses, gofmt clean, make build/lint/test green (ruff check+format clean under the widened select), semgrep --error 0 findings / 898 scanned / 0 parse errors. Signed-off-by: Brad Flaugher <brad@elcanotek.com>
The previous commit put an `issues: write` alarm job inside codeql.yml and semgrep.yml. Those are REUSABLE workflows, and a called workflow may not request token permissions its caller did not grant — and that check fires at PLAN time, before any `if: github.event_name == 'schedule'` can skip the job. Result: the calling Dev CI run on 745fe6d died with startup_failure (run 32578976517) and NOTHING scanned on that head. The "intersection semantics" assumption in that commit's comment was wrong, and this is the correction. The alarm for those two lanes now lives in scan-cron-alarm.yml, a workflow_run watcher on [CodeQL, Semgrep] completions, filtered to conclusion=failure AND event=schedule. A watcher has no caller, so it holds issues: write without widening any gate's token — a PR-path scanner failure already reddens the calling gate, and a red manual dispatch has a human watching it. govulncheck-scheduled.yml and grype-scheduled.yml keep their in-job steps: standalone workflows, no caller, no plan-time constraint. workflow_run only fires from the default branch's copy, so the alarm arms at the dev->main promotion — the same moment the crons start mattering. Signed-off-by: Brad Flaugher <brad@elcanotek.com>
… in summaries Three fixes from reading run 32579378165, the first run where the security-extended measurement actually executed. THE EXTENDED SUITE'S FIRST CATCH, AND ITS UNFLAGGED TWIN. Extended found one actions finding: actions/untrusted-checkout/medium at build-sandbox-image.yml's "Checkout fleet (build script)" step, whose `ref: inputs.fleet_ref` is caller-controlled — and the checked-out script is then EXECUTED. Reproduced locally with the same CodeQL 2.26.3 bundle and suite to get the exact location, then triaged for real: every ref of ElcanoTek/fleet is collaborator-written EXCEPT refs/pull/* (fork PRs), so a caller passing a pull-request ref would execute non-collaborator code — with a contents:read token of the calling repo, which for a private bundle repo is an exfiltration primitive. The fix is a validation step that refuses pull-request refs (and leading-dash values) and exposes the vetted value as a step output the checkout consumes; the query's trigger was a NAME heuristic (any ref: fed by a field matching .*(head|branch|ref).*), so consuming the validator's neutrally named output also clears the alert honestly — the sanitizer is genuinely in the path, not renamed around. The better half of the catch: publish-sandbox-image.yml has the IDENTICAL pattern and escaped BOTH query variants — too privileged for medium (which only reports non-privileged contexts) and no PR-event taint for high — while holding packages:write, making it the more dangerous twin. Hardened symmetrically. Verified: rebuilding the actions database and re-running the full security-extended suite locally now reports 0 findings across all 13 workflow files, and semgrep stays clean on both edited files. CANARY PATH. scripts/check-npm-overrides.sh was invoked repo-relative from the web jobs, whose default working-directory is web/ — exit 127, exactly what the gate is for. The script is cwd-free (registry queries only), so it is now invoked via $GITHUB_WORKSPACE. (The rampart audit step beside it already proved step-level working-directory resolves from the workspace root.) SUMMARIES NOW PRINT file:line PER FINDING. Run 32579378165's summary named the rule but not the location, which sent the fix hunt through a 600MB CLI bundle download. The jq now renders "[level] ruleId file:line" per finding, validated against a location-bearing SARIF fixture and the empty case. security-extended status after this commit: go, python and javascript-typescript measured clean in CI on the previous run; actions measured clean locally on the same toolchain after the hardening. All four verified in CI on this run. Signed-off-by: Brad Flaugher <brad@elcanotek.com>
… in the docs Docs-only follow-up to the last workflow commit, which shipped the changes but not their story: - docs/CODEQL.md: the matrix table now says security-extended (it still read "default suite"); the adoption section records the one finding the wider suite produced (actions/untrusted-checkout/medium on build-sandbox-image.yml), why it was fixed rather than waived (the actions language has no AlertSuppression.ql), the refs/pull/* refusal that fixes it, and the same hardening applied to publish-sandbox-image.yml — the unflagged twin with packages: write that the name-heuristic query missed. Verified clean in CI on all four languages (Dev CI run 525, id 32580031374). - docs/SCANNING.md: same story in the stack doc; the job-log example updated to the shipped file:line format plus the database-file-count coverage line; the override canary's $GITHUB_WORKSPACE invocation explained (the step runs under working-directory: web, where a repo-relative path exits 127). - CHANGELOG.md: the security-extended bullet now carries the finding, the fix in both workflows, the CI verification, and the summary/canary fixes. go test ./scripts green (the docs↔pin agreement assertions still hold). Signed-off-by: Brad Flaugher <brad@elcanotek.com>
bradflaugher
pushed a commit
that referenced
this pull request
Aug 22, 2026
The gate I added in the previous commit was VACUOUS, and the first
full-tree run proved it: run 32583247659 reported "0 blocking" over a
tree holding 30 findings, with sec-sev=0 on every one of them —
including go/request-forgery, whose real security-severity is 9.1.
Cause: CodeQL writes query metadata into runs[].tool.extensions[].rules[]
(one extension per query pack), NOT runs[].tool.driver.rules[]. The
driver is the CLI itself. Reading only the driver resolved nothing, so
every finding scored 0 and nothing could ever reach the High band. For
the same reason a result's `level` is usually absent from the result:
SARIF falls back to the rule's defaultConfiguration.level, which was
also unreachable.
Three changes, in order of importance:
1. The classifier now reads driver.rules AND extensions[].rules, and
resolves level from the rule when the result omits it. Verified
against a fixture built to the real SARIF shape.
2. A VACUITY CHECK that would have caught this: if a scan produced
findings but resolved zero rule metadata, the job fails instead of
reporting clean. "Findings but no metadata" means the lookup is
broken and the gate is evaluating nothing — the green-but-vacuous
outcome this workflow exists to rule out, which I then walked
straight into.
3. The banding is now security-severity only, with level as the fallback
for a rule that publishes no security-severity. Once metadata
resolved, banding on level as well put all 23 go/log-injection
findings (security-severity 6.1) into the blocking tier, because
nearly every CodeQL security query is @problem.severity error — level
carries no severity information for them. That would have reproduced
the any-finding deadlock by a different route.
The filter now lives in .github/codeql-gate.jq and is used by both the
summary and the gate via `jq -f`, so the thing that reports and the
thing that blocks cannot disagree about what "blocking" means — and it
can be exercised against fixture SARIF with the exact bytes CI runs.
Exercised end-to-end from the YAML: the real-shape fixture (0 blocking,
8 accepted, 2 advisory), a fresh unregistered go/request-forgery in an
unwaived file (blocks — the property that makes the per-file register
different from a query-filters exclude), the vacuity fixture (fails),
missing register, missing filter, and unparseable SARIF.
CI supply chain, from the same audit:
- Repin github/codeql-action (5 refs) and golangci/golangci-lint-action
(2 refs). Both were pinned to the ANNOTATED TAG OBJECT of a MUTABLE
major tag, not to a commit: `refs/tags/v4` -> 4c0873ef but
`refs/tags/v4^{}` -> db488dde. A tag object is immutable but only
reachable while that tag points at it, so the day upstream moves v4 —
which codeql-action does on essentially every release — the object is
unreferenced and Actions can no longer resolve the ref. A
self-inflicted CI outage with no attacker involved, armed in seven
places. Verified with `git ls-remote --tags` and repinned to the
peeled commits, with exact `# vX.Y.Z` comments (they read `# v4
(4.37.8)` and `# v9`, which dependabot-core parses as "4" and "9").
scripts/check_action_pins_test.go now enforces the shape across all
53 third-party refs.
- build-sandbox-image.yml / publish-sandbox-image.yml: replace the
fleet_ref deny-list with an allow-list. The deny-list had two holes.
GITHUB_OUTPUT newline injection: a workflow_call string input may
contain newlines and the value was printf'd unsanitized, so
fleet_ref="main\nresolved=refs/pull/1/head" matched no deny pattern,
exited 0, and emitted two `resolved=` lines — last-wins handed the
attacker the ref, and the same primitive forges any step output. And
a raw commit SHA: "every ref here is collaborator-written except
refs/pull/*" is true of named refs and false of reachable commits,
since GitHub keeps fork-PR commits in the base repo's object store
and actions/checkout will fetch a bare SHA. Both matter because these
workflows EXECUTE the checked-out build script, and the publish twin
holds packages: write with a live GHCR login. Tested: 5 legitimate
refs pass, 9 attack shapes fail, including the injection payload.
- ci.yml docs-only classifier: `*.md` matched at any depth and `docs/*`
matched everything under docs/, so a PR touching only
internal/clientconfig/builtin_skills/*/SKILL.md (go:embed'd and
asserted by three test files), config/default/system_prompts/*.md
(the shipped prompts docs/PROMPT-CACHE-CONTRACT.md exists to
protect), or docs/openapi.yaml (asserted by openapi_drift_test.go)
was classified docs-only — and every job skipped while CI gate
reported green. Narrowed to a prose allow-list.
- ci.yml ci-gate: a `skipped` job passed the gate unconditionally.
Now a skip is only accepted when the classifier actually said
docs-only; otherwise the gate refuses to pass over a suite that did
not run. Same rot pattern as red-but-not-required, colours inverted.
Gate logic tested in six directions.
- scripts/check_gate_needs_test.go: assert every job in ci.yml and
dev-ci.yml is in its aggregate gate's `needs`. Both are complete
today (11/11 and 7/7); nothing asserted it, and adding a job without
extending needs is a silent one-line regression that produces a
red-but-not-required check — the exact failure #1246 was written to
stop recurring.
Signed-off-by: Brad Flaugher <brad@elcanotek.com>
bradflaugher
pushed a commit
that referenced
this pull request
Aug 22, 2026
Dead code is audit surface: an auditor asks "why is this here, is it
reachable, is it maintained?" and every unreachable identifier costs a
round of that. Each deletion below was verified with
`git grep -w <name>` across all tracked files — every hit was the
declaration and its own doc comment, zero call sites, zero test
references, and no coupling to docs/openapi.yaml or schemaModelRegistry
(so TestOpenAPISchemaDrift cannot regress).
- internal/mcpoauth/errors.go: IsInvalidClient. The odd one out of a
four-predicate set — IsInvalidGrant, IsInvalidTarget and
IsInvalidScope all have callers. Its job is already done inline:
IsTerminalRefreshError and ReauthDetail both switch on the literal
"invalid_client" rather than calling it.
- internal/sched/apikeys/apikeys.go: Manager.LogAction. A nine-parameter
exported wrapper over the private m.logAudit that nothing calls. Worth
naming precisely because it is API-key AUDIT logging: a reviewer
grepping for audit surface lands here first and has to work out that
callers use logAudit directly. Deleting removes the ambiguity.
- internal/sched/models/models.go: TaskAssignment and LogSubmission —
the two halves of the retired v1 remote-worker protocol
(OrchestratorURL, Files/FileChecksums, the worker log POST),
superseded when the platform consolidated into one process. The live
log path uses models.LogSession directly.
- internal/sched/models/models.go: MaxLogSubmissionSize. This one is
more than clutter — it declared a 24 MB body cap that NOTHING
enforced. The cap actually applied is MaxJSONBodySize = 1 MB
(internal/sched/handlers/middleware.go, wired via
BodySizeLimitMiddleware). So the real posture was 24x stricter than
the constant claimed, and an auditor reading models.go would have
concluded fleet accepts 24 MB bodies. An unenforced-limit claim is
exactly the kind of thing that becomes a finding.
- internal/config/config.go: DefaultFromEmail. Its doc comment called
it "the fallback From address for outgoing mail", but no code path
consumes it — an operator who sets neither SENDGRID_FROM_EMAIL nor
MAILBUX_FROM_EMAIL does not get this fallback. A documented
capability that does not exist is a violation of this repo's own
honesty-in-docs invariant, so the constant goes rather than the claim
being left standing.
- internal/tools/task_tracker.go: the only commented-out code block in
the tree, plus the inProgressCount it was the sole reader of (the
counter was incremented and never read once the comment is gone).
Replaced with a sentence saying why there is deliberately no
"more than one in_progress" check.
`golangci-lint` with `default: standard` already includes `unused` and
is a full gate, so unexported dead code is structurally zero — which is
why everything above is an EXPORTED identifier in internal/, the class
`unused` deliberately does not report. Confirmed independently with
`deadcode -test -tags fleet_host_executor ./...`, which now reports
nothing.
CI permissions and the alarm:
- scan-cron-alarm.yml only fired on `conclusion == 'failure'`. That
ignores startup_failure — which is the exact failure this file's own
header describes as the incident that motivated it (an in-job alarm
variant failed a whole Dev CI run that way, so NO scanning ran on
that head) — and timed_out, which matters given codeql.yml caps at 30
minutes and semgrep.yml at 15. Now alarms on any conclusion that is
not success or skipped. Also added the daily real-model canary to the
watched list: it had no alarm at all, and a silently red daily canary
is the rot pattern this file exists to prevent. Noted that the watcher
matches on workflow DISPLAY NAME, so renaming `name:` disarms it.
- ci.yml carried `pull-requests: read` at WORKFLOW level for
golangci-lint-action's only-new-issues, which is explicitly `false`.
So the scope had no consumer while still reaching every job that does
not override it — including web, playwright and e2e-live, which
npm-install and execute thousands of third-party packages. Removed.
- screenshots.yml held `contents: write` at workflow level for a single
job that runs `npm ci` and `playwright install`. It bought nothing:
the push it existed for cannot succeed, because the main ruleset
carries a pull_request rule with current_user_can_bypass: never and no
bypass actors. So it was a repo-writable token handed to third-party
code on every run, in exchange for a guaranteed-failing push whose
commit message also carried [skip ci]. Dropped to read, with the
shape a real implementation would take written down.
- auto-merge-dependabot.yml: the header asserted "CI is the approval
signal and it is never bypassed", because `gh pr merge --auto` holds
the merge until every REQUIRED check passes. That is only true where
something is required. The dev ruleset requires no status checks at
all, and dependabot.yml points every version update at dev — so there
was nothing holding the merge. Excluded `github_actions` from
auto-merge (that ecosystem's "dependency" IS the CI definition, and
cooldown is not even available for it — Dependabot supports cooldown
for gomod and npm only), added a `branches: [main, dev]` filter so
this can never silently apply to an unprotected branch, and moved the
write scopes from workflow level onto the one job that needs them.
Getting `Dev gate` into the dev ruleset is the real fix and is a
repo-settings action; it is flagged for the owner.
- codeql.yml / dev-ci.yml: moved the two remaining `${{ }}` expressions
out of `run:` blocks and into `env:`. The values come from
{success, failure, cancelled, skipped} so nothing attacker-controlled
reached the shell, but this is the shape the two sites fixed in #1246
were fixed away FROM, and it breaks semgrep's bash sub-parser, which
silently costs coverage on the very files it appears in.
Two stale claims settled rather than left for an auditor to find:
- docs/adr/0012: `cmd/fleet-admin` was to be "a deprecation shim for ONE
release ... removed next release". That clock never started — `git
tag` returns nothing, VERSION is 0.0.0, and CHANGELOG.md has only an
[Unreleased] heading, so "next release" is not a date. The shim also
turns out to be load-bearing rather than vestigial: the Makefile,
bootstrap.sh, update.sh and fleet-upgrade.sh all build or install it,
the last two hard-fail without it, and scripts_dryrun_test.go asserts
the "would install fleet + fleet-admin" string. Amended with a
concrete trigger — removed in the first release after 1.0.0 — and the
note that it forks no logic (it shares internal/admincli.Run).
docs/EVENT-TRIGGERS.md and docs/openapi.yaml were still teaching
`fleet-admin sched trigger …` as the primary command for HMAC-secret
rotation; those are security procedures, so they now say `fleet`.
- migration 022 carried the tree's only TODO(security) — and every
auditor greps for that string. Two problems beyond the deferred work:
it sat in an APPLIED migration, so it was parked where nobody can
close it in place, and it pointed at a source file in an unrelated
external codebase (a dangling cross-repo pointer in a security note).
Rewritten to state the fact plainly (the column holds account NAMES,
never credential values, which are brokered host-side per ADR-0003 /
ADR-0042), to say that whether account names are themselves in scope
is an open threat-model question for the owner, and to point at
SECURITY.md as where that gets answered. golang-migrate tracks by
version with no checksum, so editing the comment cannot re-run or
invalidate the applied DDL. The tree now has zero
TODO/FIXME/XXX/HACK markers.
- .golangci.yml's noctx exclusion said "the one production noctx
(cmd/fleet-admin bootstrap) is fixed in code via exec.CommandContext".
cmd/fleet-admin has held no exec call since the CLI was unified in
#461; the real call sites are in cmd/fleet and internal/admincli.
Repointed, because a lint suppression whose stated reason names dead
code is a suppression nobody can re-verify.
- scripts/generate-icons.py declared web/src/app/favicon.ico among its
outputs. That file has never been committed and is not gitignored
either, so it existed only on whoever last ran the script — while
every other declared output IS committed. The App Router serves
icon.svg and apple-icon.png, with favicon-16/32.png under public/, so
the .ico had no consumer. Dropped, and the docstring now records that
the outputs are committed and when to regenerate.
Signed-off-by: Brad Flaugher <brad@elcanotek.com>
bradflaugher
pushed a commit
that referenced
this pull request
Aug 22, 2026
An auditor reads the docs and then checks the code, so a doc that overstates is worse than one that says less. This repo has an explicit "Honesty in docs" invariant; these are the places it had drifted. The load-bearing corrections: - AGENTS.md said "Everything is at zero findings today; keeping it there is the point." False, and it was the one line in the agent-facing operating guide an auditor would hold up. The thresholds now differ by scanner, and the difference is stated: Semgrep, ruff and npm audit gate on any finding; CodeQL gates on the High band minus the reviewed register, with everything below it advisory. Also records the two facts an agent must not get wrong — a pull_request CodeQL run certifies a DIFF and never a tree, and `Dev gate` is not a required check on `dev`. - docs/CODEQL.md carried a whole Triggers section describing a workflow that no longer exists (it showed `push: [main]` and `pull_request: [main, dev]`; the file has only workflow_call, workflow_dispatch and schedule), plus the assertion that a push to dev "would re-analyze identical content". That reasoning is exactly the trap that broke dev — the push run is full-tree and the PR run is diff-informed, so they are not identical content — and it is now preserved as the error rather than the rule. Also removed "a CodeQL job with a hundred open alerts still exits 0 and reports green" (false since the Fail-on-findings step) and the sample log format that never matched what the workflow prints. - docs/SCANNING.md claimed the extended suite "reports zero findings on this tree (verified in CI across all four languages on Dev CI run 525)" and that a green check means "clean tree". Run 525 was a pull_request event. Corrected, with the run-527 numbers, and the known-gaps section rewritten — the dev-ruleset gap is now the FIRST gap, since it is the one that makes several other statements in the file conditional. - SECURITY.md had no SAST section at all, though CodeQL and Semgrep are the controls an enterprise auditor asks about by name. It now has one. Its Grype paragraph claimed the gate covers "the image's RPM or Python packages" at a fixable CRITICAL; the policy script selects `.artifact.type == "rpm"` and fires on CRITICAL *and* HIGH, so Python dist-info records are reported and deliberately do not gate — both halves were wrong in the direction that overstates coverage. Its supply-chain section omitted the npm CVE gate entirely. And "CI runs gitleaks on every push" is not true of a feature branch, which runs nothing. - docs/TESTING.md's lane table said the fast lane SKIPS CodeQL. It runs both scanners. The table also omitted four lanes that now block. - CONTRIBUTING.md contradicted itself inside one sentence: "fails the build on a fixable CRITICAL or HIGH CVE ... (HIGH and below are reported, not blocking)". - CHANGELOG.md's [Unreleased] section carried five overlapping entries from #1246 that contradicted each other — one said the scanners gate through ci-gate, another said "CodeQL and Semgrep stay advisory"; one said `ruff format` is reported but not gated, another said it gates; one said Semgrep ships only p/github-actions, another all four packs; one said the code-quality suite was restored when it was dropped. A reader could not tell which was current. Collapsed into one entry describing the shipped end state, and extended with this PR's work. - ruff.toml's "<- what we gate on" marker pointed at the default-only rule line while `select` includes B, SIM and S. - Makefile's .PHONY omitted lint-python. - README.md's documentation table did not list SCANNING.md or CODEQL.md — the two most audit-relevant docs, unreachable from the README — and its layout tree was abridged without saying so. ADR housekeeping, both mine: - ADR-0036 presents its host-side exception list as exhaustive, and an auditor reads it that way, so it has to be. It still named `fastio_upload` as a host-read exception; there is no such native tool any more (Fast.io is an MCP server behind the broker), so the ADR was claiming a hole that does not exist. Two real classes were missing: host `git worktree` management on the scheduled-run path, and the admin-gated host `podman` build for the rampart install. Neither is model-authored and neither weakens the invariant — but "is this exception in the ADR?" should have a reliable answer, and now does. - ADR-0048's two counts were off and are now measured, not recalled: 625 `_test.go` files (not 621) and 81 `//nolint:gosec // G706` sites (not 77 — four of the increase are this branch's own). Verification: make build, make lint (golangci-lint v2.13.1 + ruff + migration DDL lint) and make test all clean; web oxlint, tsc, vitest (1104 tests) and next build all clean. Every markdown link target in every file touched here was checked to resolve on disk. Two lint findings from my own earlier commits fixed here rather than left standing: four `//nolint:gosec // G706` directives I had added were UNUSED — once the value goes through logSafe, gosec stops flagging the line, so nolintlint was right and an unused suppression is worse than none — and a prealloc nit in the new gate-needs test. Signed-off-by: Brad Flaugher <brad@elcanotek.com>
bradflaugher
added a commit
that referenced
this pull request
Aug 22, 2026
…oles closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended.
bradflaugher
added a commit
that referenced
this pull request
Aug 23, 2026
* Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * 📝 Point the Kubernetes docs at the example bundle for that path fleet links to example-config in four places, but nothing referenced example-kubernetes-config — so an operator reading DEPLOYMENT-KUBERNETES.md had no pointer to a worked bundle for the path they were on, even though one now exists and carries the two Containerfiles, a documented values overlay for this chart, and an empty-cluster-to-working-fleet walkthrough. Four entry points, chosen because each is somewhere a reader lands cold: the Kubernetes guide's intro, the chart README (which renders a control plane but supplies no bundle for it to load), the two README spots where the template is introduced as one of the three ways in, and AGENTS.md's Kubernetes row. Framed as peers rather than parent and child, since that is what they are, and linked rather than vendored — client content stays out of this repo per the coupling doctrine. Docs only. Both URLs verified to resolve (HTTP 200; the repo is public). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com>
bradflaugher
added a commit
that referenced
this pull request
Aug 23, 2026
…or that path (#1260) * Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * 📝 Point the Kubernetes docs at the example bundle for that path fleet links to example-config in four places, but nothing referenced example-kubernetes-config — so an operator reading DEPLOYMENT-KUBERNETES.md had no pointer to a worked bundle for the path they were on, even though one now exists and carries the two Containerfiles, a documented values overlay for this chart, and an empty-cluster-to-working-fleet walkthrough. Four entry points, chosen because each is somewhere a reader lands cold: the Kubernetes guide's intro, the chart README (which renders a control plane but supplies no bundle for it to load), the two README spots where the template is introduced as one of the three ways in, and AGENTS.md's Kubernetes row. Framed as peers rather than parent and child, since that is what they are, and linked rather than vendored — client content stays out of this repo per the coupling doctrine. Docs only. Both URLs verified to resolve (HTTP 200; the repo is public). Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com>
bradflaugher
added a commit
that referenced
this pull request
Aug 23, 2026
* Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * Promote dev → main: point the Kubernetes docs at the example bundle for that path (#1260) * Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * 📝 Point the Kubernetes docs at the example bundle for that path fleet links to example-config in four places, but nothing referenced example-kubernetes-config — so an operator reading DEPLOYMENT-KUBERNETES.md had no pointer to a worked bundle for the path they were on, even though one now exists and carries the two Containerfiles, a documented values overlay for this chart, and an empty-cluster-to-working-fleet walkthrough. Four entry points, chosen because each is somewhere a reader lands cold: the Kubernetes guide's intro, the chart README (which renders a control plane but supplies no bundle for it to load), the two README spots where the template is introduced as one of the three ways in, and AGENTS.md's Kubernetes row. Framed as peers rather than parent and child, since that is what they are, and linked rather than vendored — client content stays out of this repo per the coupling doctrine. Docs only. Both URLs verified to resolve (HTTP 200; the repo is public). Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> * 🚨 Stop the Connections focus assertions sampling a single tick One of these failed a docs-only PR yesterday, reporting document.activeElement as <body> where the API-key input was expected. Nothing in that diff could reach the web tier, and the same suite passed locally 1104/1104, so it was a flake — but a flake nobody had pinned down, and it would have bitten the next person just as arbitrarily. The mechanism, confirmed rather than guessed. The deep-link test awaits findByTestId("dir-form-browserbase") and then asserts focus inline. The form opening and the focus landing are not the same event: the state update comes from the catalog fetch resolving OUTSIDE act, so React schedules the card's apiKeyRef focus effect on a macrotask, while findByTestId resolves the instant the form NODE appears — a commit earlier. I proved the window is real by watching the raw DOM through a MutationObserver outside RTL's act wrapper: at the moment the form node appears, the input exists and activeElement is not yet it. On an unloaded machine the effect flush wins that race every time (0/25 repeats, and 0/12 under saturating CPU load, which is why it does not reproduce locally); on a loaded CI runner it can lose. So the assertion was never testing "focus ends up in the key field" — it was testing "focus has already landed at this particular instant", which is not a property the component promises or a user could perceive. Retrying it through waitFor tests the guarantee that actually matters. Two sibling assertions in the same file had the identical shape — dialog focus after findByRole, and the focus hand-back after the dialog unmounts. Neither has flaked yet; both could, for the same reason, so both are fixed now rather than after they do. Deliberately NOT changed: the focus assertions in Menu, Toast and admin/users. Those follow a synchronous fireEvent or a direct .focus(), which React flushes inside act, so they are not racy — waitFor there would be noise. The distinguishing factor is focus driven by an effect after an ASYNC state update. Kept its teeth, checked rather than assumed: removing the apiKeyRef focus() from the component still fails the deep-link assertion, and removing panelRef.focus() still fails the dialog one. 20/20 repeats green afterwards, plus npm run lint, typecheck and the full 1104-test suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com>
bradflaugher
added a commit
that referenced
this pull request
Aug 23, 2026
…single tick (#1262) * 📝 Point the Kubernetes docs at the example bundle for that path (#1259) * Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * 📝 Point the Kubernetes docs at the example bundle for that path fleet links to example-config in four places, but nothing referenced example-kubernetes-config — so an operator reading DEPLOYMENT-KUBERNETES.md had no pointer to a worked bundle for the path they were on, even though one now exists and carries the two Containerfiles, a documented values overlay for this chart, and an empty-cluster-to-working-fleet walkthrough. Four entry points, chosen because each is somewhere a reader lands cold: the Kubernetes guide's intro, the chart README (which renders a control plane but supplies no bundle for it to load), the two README spots where the template is introduced as one of the three ways in, and AGENTS.md's Kubernetes row. Framed as peers rather than parent and child, since that is what they are, and linked rather than vendored — client content stays out of this repo per the coupling doctrine. Docs only. Both URLs verified to resolve (HTTP 200; the repo is public). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> * 🚨 Stop the Connections focus assertions sampling a single tick (#1261) * Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * Promote dev → main: point the Kubernetes docs at the example bundle for that path (#1260) * Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * 📝 Point the Kubernetes docs at the example bundle for that path fleet links to example-config in four places, but nothing referenced example-kubernetes-config — so an operator reading DEPLOYMENT-KUBERNETES.md had no pointer to a worked bundle for the path they were on, even though one now exists and carries the two Containerfiles, a documented values overlay for this chart, and an empty-cluster-to-working-fleet walkthrough. Four entry points, chosen because each is somewhere a reader lands cold: the Kubernetes guide's intro, the chart README (which renders a control plane but supplies no bundle for it to load), the two README spots where the template is introduced as one of the three ways in, and AGENTS.md's Kubernetes row. Framed as peers rather than parent and child, since that is what they are, and linked rather than vendored — client content stays out of this repo per the coupling doctrine. Docs only. Both URLs verified to resolve (HTTP 200; the repo is public). Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> * 🚨 Stop the Connections focus assertions sampling a single tick One of these failed a docs-only PR yesterday, reporting document.activeElement as <body> where the API-key input was expected. Nothing in that diff could reach the web tier, and the same suite passed locally 1104/1104, so it was a flake — but a flake nobody had pinned down, and it would have bitten the next person just as arbitrarily. The mechanism, confirmed rather than guessed. The deep-link test awaits findByTestId("dir-form-browserbase") and then asserts focus inline. The form opening and the focus landing are not the same event: the state update comes from the catalog fetch resolving OUTSIDE act, so React schedules the card's apiKeyRef focus effect on a macrotask, while findByTestId resolves the instant the form NODE appears — a commit earlier. I proved the window is real by watching the raw DOM through a MutationObserver outside RTL's act wrapper: at the moment the form node appears, the input exists and activeElement is not yet it. On an unloaded machine the effect flush wins that race every time (0/25 repeats, and 0/12 under saturating CPU load, which is why it does not reproduce locally); on a loaded CI runner it can lose. So the assertion was never testing "focus ends up in the key field" — it was testing "focus has already landed at this particular instant", which is not a property the component promises or a user could perceive. Retrying it through waitFor tests the guarantee that actually matters. Two sibling assertions in the same file had the identical shape — dialog focus after findByRole, and the focus hand-back after the dialog unmounts. Neither has flaked yet; both could, for the same reason, so both are fixed now rather than after they do. Deliberately NOT changed: the focus assertions in Menu, Toast and admin/users. Those follow a synchronous fireEvent or a direct .focus(), which React flushes inside act, so they are not racy — waitFor there would be noise. The distinguishing factor is focus driven by an effect after an ASYNC state update. Kept its teeth, checked rather than assumed: removing the apiKeyRef focus() from the component still fails the deep-link assertion, and removing panelRef.focus() still fails the dialog one. 20/20 repeats green afterwards, plus npm run lint, typecheck and the full 1104-test suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com>
bradflaugher
added a commit
that referenced
this pull request
Aug 27, 2026
), scheduled-run file tools (#1290), sealed warm pool (#1291), audit-abort fix, dep bumps (#1293) * 📝 Point the Kubernetes docs at the example bundle for that path (#1259) * Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * 📝 Point the Kubernetes docs at the example bundle for that path fleet links to example-config in four places, but nothing referenced example-kubernetes-config — so an operator reading DEPLOYMENT-KUBERNETES.md had no pointer to a worked bundle for the path they were on, even though one now exists and carries the two Containerfiles, a documented values overlay for this chart, and an empty-cluster-to-working-fleet walkthrough. Four entry points, chosen because each is somewhere a reader lands cold: the Kubernetes guide's intro, the chart README (which renders a control plane but supplies no bundle for it to load), the two README spots where the template is introduced as one of the three ways in, and AGENTS.md's Kubernetes row. Framed as peers rather than parent and child, since that is what they are, and linked rather than vendored — client content stays out of this repo per the coupling doctrine. Docs only. Both URLs verified to resolve (HTTP 200; the repo is public). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> * 🚨 Stop the Connections focus assertions sampling a single tick (#1261) * Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * Promote dev → main: point the Kubernetes docs at the example bundle for that path (#1260) * Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * 📝 Point the Kubernetes docs at the example bundle for that path fleet links to example-config in four places, but nothing referenced example-kubernetes-config — so an operator reading DEPLOYMENT-KUBERNETES.md had no pointer to a worked bundle for the path they were on, even though one now exists and carries the two Containerfiles, a documented values overlay for this chart, and an empty-cluster-to-working-fleet walkthrough. Four entry points, chosen because each is somewhere a reader lands cold: the Kubernetes guide's intro, the chart README (which renders a control plane but supplies no bundle for it to load), the two README spots where the template is introduced as one of the three ways in, and AGENTS.md's Kubernetes row. Framed as peers rather than parent and child, since that is what they are, and linked rather than vendored — client content stays out of this repo per the coupling doctrine. Docs only. Both URLs verified to resolve (HTTP 200; the repo is public). Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> * 🚨 Stop the Connections focus assertions sampling a single tick One of these failed a docs-only PR yesterday, reporting document.activeElement as <body> where the API-key input was expected. Nothing in that diff could reach the web tier, and the same suite passed locally 1104/1104, so it was a flake — but a flake nobody had pinned down, and it would have bitten the next person just as arbitrarily. The mechanism, confirmed rather than guessed. The deep-link test awaits findByTestId("dir-form-browserbase") and then asserts focus inline. The form opening and the focus landing are not the same event: the state update comes from the catalog fetch resolving OUTSIDE act, so React schedules the card's apiKeyRef focus effect on a macrotask, while findByTestId resolves the instant the form NODE appears — a commit earlier. I proved the window is real by watching the raw DOM through a MutationObserver outside RTL's act wrapper: at the moment the form node appears, the input exists and activeElement is not yet it. On an unloaded machine the effect flush wins that race every time (0/25 repeats, and 0/12 under saturating CPU load, which is why it does not reproduce locally); on a loaded CI runner it can lose. So the assertion was never testing "focus ends up in the key field" — it was testing "focus has already landed at this particular instant", which is not a property the component promises or a user could perceive. Retrying it through waitFor tests the guarantee that actually matters. Two sibling assertions in the same file had the identical shape — dialog focus after findByRole, and the focus hand-back after the dialog unmounts. Neither has flaked yet; both could, for the same reason, so both are fixed now rather than after they do. Deliberately NOT changed: the focus assertions in Menu, Toast and admin/users. Those follow a synchronous fireEvent or a direct .focus(), which React flushes inside act, so they are not racy — waitFor there would be noise. The distinguishing factor is focus driven by an effect after an ASYNC state update. Kept its teeth, checked rather than assumed: removing the apiKeyRef focus() from the component still fails the deep-link assertion, and removing panelRef.focus() still fails the dialog one. 20/20 repeats green afterwards, plus npm run lint, typecheck and the full 1104-test suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> * ⬆️ Bump the go-minor-patch group: bubbletea 2.0.9, fantasy 0.41.2 (#1254) Direct bumps: charm.land/bubbletea/v2 2.0.8 → 2.0.9 and charm.land/fantasy 0.41.1 → 0.41.2. Both are patch releases: bubbletea fixes MouseButton11 and media-record key mapping, a ProgressBarState.String() panic, a cursedRenderer pendingErase artifact and the kitty keyboard stack on exit; fantasy corrects Bedrock SSO auth priority and stops emitting empty reasoning_content fields on assistant tool-call messages. Transitive updates ride along: the AWS SDK v2 set, cloud.google.com/go/auth, anthropic-sdk-go, enterprise-certificate-proxy, google.golang.org/api, genai, genproto, protobuf 1.36.12 and testify 1.12.0. go.mod and go.sum only — no source changes. Verified on top of current dev: make build, make test (full suite against Postgres), make lint (Go + ruff) and make govulncheck all clean; govulncheck reports 0 vulnerabilities called. The PR branch was updated onto dev's head first, so the green Dev gate certifies this exact merge result. * agent,runner: extract RunTurn and executeTask into phase helpers TLDR: RunTurn (275 lines) and executeTask (305 lines) juggled enough state that two confirmed audit bugs (#1105, #1117) partly stemmed from them (#1127). Both now read as narratives of named phases -- RunTurn is 153 lines calling 7 helpers, executeTask is 180 calling 6 -- with every extracted body line-identical to its original span modulo the mechanical edits extraction forces (parameter threading, return plumbing, unindent). Zero logic changes, zero defer-scope changes. Fix: RunTurn gained admitInteractiveTurn, composeTurnSystemPrompt, assembleTurnMessages, interactiveRunSelection, openTurnRemoteOverlay, failedTurnResult, and completedTurnResult (mirroring the pre-existing cancelledTurnResult); executeTask gained buildTaskRunContext, captureRunFailure, parkForQuestion, finishStopped, finishLeaseLost, and finishSuccess (joining the existing per-outcome family). Function- exit defers stayed in the parents: the limiter release and overlay close are returned/re-registered by RunTurn under the exact HEAD conditions, and the sandbox/workspace + MCP-scope acquisition block was deliberately NOT extracted because it owns three such defers. finishSuccess's absorbed returns are equivalent because nothing follows the terminal switch; the terminal frame flows to the parent's deferred emit through the same map reference. Tests: zero test files touched -- the ADR-0035, #1116 lease/zombie, behavioral net. Equivalence proven mechanically: an AST inventory shows only the two parent functions' hashes changed (+13 helpers, all other declarations byte-identical), and three adversarial reviewers reproduced the inventory and hand-diffed all 13 extracted bodies against their HEAD spans (no undeclared differences) plus the full defer audit. Test rounds: both packages -count=1/-count=2/-race, agentcore/scheduledrun/httpapi/cmd-fleet dependents, full make test with both schemas -- green except the podman-gated sandbox integration tests (no fleet-sandbox image on this box; identical on dev). gofmt/vet/golangci-lint clean, no new suppressions. Signed-off-by: jzhao234 <junzhao234@gmail.com> * sched: encode the task lifecycle as a tested transition table TLDR: transition rules lived only in scattered WHERE status=... clauses across claim/recovery/serialization/reporting queries -- every new status touched them all by hand, and nothing enumerated the legal edges (#1127). The lifecycle is now a 49-edge tested constant (internal/sched/models/task_lifecycle.go) with derived status sets, init-time validation, and coupling tests that turn drift into red tests. No behavior change: the only runtime derivation is claim.go's already-named taskActiveStatuses now mapping the table-derived models.ActiveTaskStatuses (same {leased, running} set; the IN-list placeholder pin proves it). Fix: the table records every edge the GUARDED runtime writers can produce -- birth, due-sweep, run_if settlement, claim, worker report, retry requeue, dead-letter (in-process and #1116 recovery), pause/ask + expiry, wake/park + expiry + 24h backstop, cancel, DLQ replay, and the editable/replace re-derivations -- each edge naming its authoritative writer and file. Coupling is machine-checked at the strongest seam available per writer, honestly labeled: behavioral writer matrices drive each db/storage transition writer against a row in EVERY status and assert the outcome matches the table; an AST scan proves every status literal in the packages' tasks-table SQL is a known status; set pins cover cleanup, serialization placeholders, the scheduler's parametric callers, and the legacy-import births (admincli's validSchedTaskStatus === the table's birth To-set, both directions). Completeness validation (reachability, no non-terminal dead ends, terminal exits guarded-only) runs at package init and as a test. Deliberately OUTSIDE the model and documented as such: the two verbatim-upsert import paths (sched task import, legacy import --overwrite) can produce any->imported-status past every guard -- pre-existing restore surgery recorded, not changed. Findings encoded, not fixed (current reality preserved): cancel can erase a DLQ row's replayability (dead_lettered->cancelled is live); terminal refusal lists are inconsistent across writers; leased-> terminal edges are reachable because a failed running-report only logs; IsValidReportedStatus has zero production callers (the worker- report to-side is caller discipline, not a guard -- documented). Tests: red-cased three ways during review (edge removal fails the storage matrix; a guard change without a table edit fails the db matrix; a resurrected retired-status literal fails the scan); three adversarial reviewers independently re-inventoried every status- writing path and confirmed the guarded edges match reality exactly. Lifecycle tests -count=2; full sched tree + runner + admincli green; full make test green with both schemas except the podman-gated sandbox integration tests (no fleet-sandbox image on this box; identical on dev). gofmt/vet/golangci-lint clean. Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: unify the two-plane admin permission Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: put unified admin permission first Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: show viewer before member Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: call active roles contributors Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: assign permissions when creating users Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: describe permissions on hover Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: show role help immediately Signed-off-by: jzhao234 <junzhao234@gmail.com> * ci: check overrides against locked parents Signed-off-by: jzhao234 <junzhao234@gmail.com> * Anchor scheduled-run file staging to the worktree; stop confirm_audit aborts from failing finished work (#1280) download_url resolved a relative output_dir against the process cwd in scheduled runs, then refused its own path as escaping the worktree; runs with a forced working dir now also get a working-directory message tail so MCP file tools receive an absolute output_dir. Unbound same-tool re-audits supersede instead of stacking, an abort after all declared work executed is refused instead of flagging the run terminal, and an abort no longer requires the critical_actions unlock list. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * Support multiple logins ("seats") for hosted MCP connections (#988) (#1281) A user can hold several logins to one hosted MCP server — a work and a personal GitHub, two Gamma workspaces — each its own row with its own sealed credential and share grants, and choose which one a chat or a scheduled task uses. Mirrors the bundled <VAR>_<ACCOUNT> seat model. - Migration 051: remote_mcp_servers.account + is_default; uniqueness per (user, name, account); one default per name (partial unique index); every existing row becomes its name's default. - Migration 052: conversations.mcp_accounts — per-conversation seat override for the chat Tools picker (bundled connectors too). - Runtime: agent.RemoteMCPSelection (filter / pins / exact) replaces the enabled-set everywhere an overlay opens; exactly one seat per name is mounted, registered as RegisteredMCPName(name, account); a pinned seat that is not connected is skipped, never replaced by another account. - Broker protocol: RemoteScopeSpec gains accounts/exact (labels only). - Chat: overrides ride in MCPAccountDefaults; RunTurn rebinds the approval stager with the composite broker + mounted seats so cards record the seat that ran; approval execution against a hosted connection reopens that exact seat (remote half of #167 residual 2). - Tasks: mcp_selection may pin a hosted seat; hosted names route to the overlay instead of the bundle binder; unknown names still fail loudly. - API: POST /remote-mcp-servers accepts account; POST /{id}/default; PUT /{id}/account; pickers list one entry per name with accounts / default_account / account; POST /conversations/{id}/mcp-servers accepts accounts (unknown seat = 400); first POST /chat accepts mcp_accounts. - Web: Connections groups seats per name (Set default / Rename / Add another account); chat and task pickers gain a seat select. - Docs: docs/REMOTE-MCP-MULTI-LOGIN.md, ADR-0050, CHANGELOG. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * deps(go): bump github.com/go-chi/chi/v5 from 5.3.1 to 5.3.2 in the go-minor-patch group (#1277) Bumps the go-minor-patch group with 1 update: github.com/go-chi/chi/v5 5.3.1 -> 5.3.2. Signed-off-by: dependabot[bot] <support@github.com> * deps(web): bump next from 16.3.1 to 16.3.2 in /web in the npm-minor-patch group (#1278) Bumps the npm-minor-patch group in /web with 1 update: next 16.3.1 -> 16.3.2. Signed-off-by: dependabot[bot] <support@github.com> * Audit: an abort retires abandoned commitments; the confirm trailer names outstanding work (#1282) Field case (Energizer daily, 2026-08-25 16:11 UTC, task 9847380d): the audit declared the inline mcp_pages_update_page_data; the payload had gone by reference; mcp_pages_update_page_data_upload was BLOCKED as undeclared; the model aborted (correctly — nothing had executed), re-audited the upload tool and published v572. Finish enforcement still demanded the inline declaration, the model's only exit was a second abort, and a live page landed as error. - confirm_audit(success=false) now zeroes every declared-but-unexecuted commitment (typed and legacy) and drops blocked calls awaiting retry, and its response names what it retired. A later confirm_audit(success=true) already clears the terminal flag, so the run is judged on what executes after the re-audit; an abort AFTER that execution still hits the completed-work refusal from #1280. - The success trailer described the wrong ledger: a fresh declaration used to come back "All 0 critical actions executed. Finish now." It now names the outstanding call(s) to make, and counts actual executed critical calls. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * deps(web): bump @testing-library/user-event (#1284) Bumps the npm-minor-patch group in /web with 1 update: [@testing-library/user-event](https://github.com/testing-library/user-event). Updates `@testing-library/user-event` from 14.6.5 to 14.6.6 - [Release notes](https://github.com/testing-library/user-event/releases) - [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md) - [Commits](testing-library/user-event@v14.6.5...v14.6.6) --- updated-dependencies: - dependency-name: "@testing-library/user-event" dependency-version: 14.6.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Shared files: native cross-chat file library (#1292) Admin-published files every conversation's agent can read, on both sandbox backends: canonical bytes host-side under DataDir, a read-only staged tree under the workspace root (nested :ro bind on podman, read-only subPath of the workspace claim on kubernetes), a self-healing reconciler, a per-turn prompt block, member list/download + admin manage API and Settings page, and the live shared_files_max_total_mb cap. Migration 053. Also fixes chat attachments under the kubernetes backend (staged into the conversation workspace at send time, since pods cannot see the uploads root), with the staging path defending its own path/log boundaries (CodeQL alerts 148/149 fixed in code). Design note: docs/SHARED-FILES.md * Guard user-influenced values in two RunTurn log lines (#1294) CodeQL's diff-informed run on the dev-to-main promotion flagged log-injection flows in RunTurn's re-auth notice (user-named remote MCP servers + caller email) and stream-failure line (user-selected model slug, error text, and the reason derived from it). All advisory, fixed anyway: every value passes a CR/LF-stripping guard — spelled with strings.ReplaceAll, the sanitizer go/log-injection actually models, so the alerts close instead of standing open as false positives — and the slug is %q-quoted. * health: make the /readyz sandbox probe backend-aware (#1287) Under FLEET_SANDBOX_BACKEND=kubernetes the sandbox readiness check ran `podman --version` on the control plane — a binary that shape never has — so every healthy install reported 207 degraded. The probe now reports on what sandboxes actually run on: one bounded (5s) apiserver GET /version through a new narrow KubernetesBackend.ApiserverVersion accessor, memoized by the generalized cachedProbe so the #215 unauthenticated-endpoint bound covers both backends. A missing backend handle reports an error — never ok, never a podman fallback. Podman path byte-for-byte unchanged. Refs #1264 (finding 2), #989. * validate-config: probe /api/v1/key so a bad API key actually fails (#1289) The model_api check GET'd OpenRouter's PUBLIC /api/v1/models, so any non-empty key was blessed with "API key authenticates" — a mis-created 64-hex junk secret passed validate-config and then 401'd on the first real completion. The check now probes GET /api/v1/key (401 on a bad key, 200 otherwise), converging with the provider probe's existing endpoint; warning-not-blocking posture, skips, base-URL override, and the never-print-the-key property unchanged. internal/fakellm serves /api/v1/key with the real auth contract so E2E seams don't 404. Refs #1264, #989. * sandbox: make FLEET_SANDBOX_WARM_SIZE=0 mean "no warm pool" (#1288) warmSize 0 was inexpressible: config defaulted the field to 0 and resolveWarmSize treated <=0 as "derive 2..8", so a kind-cluster overlay documenting "no warm pool" ran a two-pod Guaranteed-QoS pool anyway. The default becomes a -1 unset sentinel: unset derives (unchanged), 0 disables the warm pool, positive pins, below -1 fails loudly at load. Chart warmSize defaults null (omit env), emits any set value including 0, schema rejects negatives. Semantic change called out in the CHANGELOG. Review note for a follow-up: the negative-value guard sits as a post-load check instead of a min bound on the #1119 knob-registry row, so an explicit -1 is accepted as "derive" and ValidateEnvKnobs reports negatives as well-formed; min: bound(0) on the registry row would close both. Refs #1264 (finding 3), #989, #181. * docs: link email report onboarding runbook (#1286) Point AGENTS.md and CONNECTOR-ONBOARDING.md at the external canonical SES/S3 email-report onboarding runbook; client identifiers stay in the client bundle, per the engine/bundle coupling doctrine. * sandbox: ride client-go for kubernetes exec streaming (#1285) The hand-rolled v4.channel.k8s.io exec client lost stdin nondeterministically for multi-KB payloads on the first real cluster it met (#1264 kind rehearsal): the 28KB bridge upload wedged ~4 of 5 attempts and no tool call could run. Exec streaming now rides client-go's remotecommand WebSocket executor (v5, real stdin half-close), proven on the same cluster/pod/payloads. Adoption is deliberately narrow: client-go is a TRANSPORT for exec only — pod CRUD and the fail-closed preflight stay on the hand-rolled REST client, and the rest.Config is built from material fleet's strict kubeconfig parser already validated (clientcmd never invoked; exec plugins and insecure-skip-tls-verify still refused). ADR-0049 amended in place (its recorded revisit trigger fired). Hardened per review before merge: cappedBuffer is mutex-guarded and read via snapshot() copies (client-go abandons its copy goroutines on a cancelled stream — a reproduced -race failure on the cancelled-bash path); writeStdin is bounded so a stalled dial can never park the sandbox mutex forever; rest.Config pins Proxy off so exec never asymmetrically honors HTTPS_PROXY while pod CRUD ignores it. Regression tests pin all three. Refs #1264, #989. * ci: allow dispatching the full gate manually (#1295) pull_request webhook deliveries were dropped during the 2026-08-26 GitHub incident: the dev->main promotion PR's opened event was lost and five synchronize events never spawned a run, leaving the required "CI gate" check impossible to produce without empty commits or close/reopen — both forbidden. workflow_dispatch runs the identical suite on the branch head, so the name-matched required check is satisfied honestly; the docs-only classifier already treats un-diffable events as "run everything". * sandbox: seal the warm pool under lockdown and let sealed takes claim it (#1291) Under FLEET_DEFAULT_NETWORK_MODE=lockdown two defects shared one cause. Warm spawns (Pool.fill and Take's no-slot cold start) used PoolConfig.Container verbatim, so the pool parked OPEN-egress sandboxes (NoNetwork=false) — while every take under fleet-wide lockdown (the interactive lockdown branch, approved bash, and scheduled runs) forced a sealed cold start via TakeContainer. The result: N warm sandboxes of dead reserved cpu/memory/disk that nothing could ever claim, respawned forever by the TTL keeper, plus a cold-start latency tax on every lockdown turn. On the kubernetes backend the parked pods were labeled fleet.elcanotek.com/egress=open directly beside a boot log line claiming every pod is labeled none. The fix makes the warm pool useful under lockdown instead of deleting it: - warmContainerConfig seals warm spawns (NoNetwork=true) when the pool's fleet-wide mode is lockdown on a container backend. This alone makes the k8s egress=none label claim true by construction and the podman warm containers sealed. ModeHost is excluded — no network namespace to seal, and host pools never reach the container takes. - TakeContainerWithOverrides routes a sealed, zero-override take on a lockdown pool to Take(): the parked sandbox already has the exact posture and pool-default ceilings the caller wants, and Take's own no-slot fallback cold-starts through warmContainerConfig, so the sealed posture holds warm or cold. Every other combination keeps today's behavior exactly: any resource override cold-starts (per-task ceilings need a fresh container), a sealed take on a NON-lockdown pool still cold-starts sealed (that warm inventory is open, and a sealed take must never receive an open sandbox), and allowlisted takes are untouched (the per-turn proxy token requires a cold start). Caller audit for the posture invariant: under fleet-wide lockdown no open take of the warm pool exists — takeTurnSandboxFrom, takeTaskSandbox, and takeStagedBashSandbox all route lockdown to TakeContainer and reach plain Take() only on ErrContainerUnavailable (an image-less host/mock pool, where there is no network namespace to seal) or in non-lockdown modes (where warm spawns stay open). TakePersistent is unreachable under fleet-wide lockdown (the lockdown branch precedes the persistent borrow in every caller). Doc truth restored alongside: the "always cold-starts" comments on TakeContainer / TakeContainerWithOverrides, the container.go :z SELinux comment's lockdown sentence (the shared-label rationale is unaffected; the behavioral claim was about to become a lie), the kubernetes boot log line that lied before, and the sandbox-probe / scheduledrun taker comments. Tests pin all four behaviors against the fake kubernetes apiserver (pod egress labels + pod identity distinguish warm claim from cold start) and the podman-shared warmContainerConfig seam: lockdown warm spawns are sealed, lockdown TakeContainer claims a warm sandbox, an open pool's sealed take still cold-starts sealed, and overrides still cold-start under lockdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GhoJPvJfQGVoucCyczmEkR * tools/taskrun/scheduledrun: let scheduled and one-shot runs use the file tools and read bundle docs (#1290) Three stacked, backend-independent gaps made every scheduled run's and fleet task run's file-tool calls fail — workspace writes in the one-shot harness, and view_file protocols/... everywhere: (a) internal/taskrun never registered the workspace root or the supporting-doc dirs, so tools.ValidatePath fell back to its legacy process-cwd allowlist (/opt/fleet/client in the split control-plane image) and rejected every workspace-relative path. taskrun now registers both globals with its minted workspace, exactly like the serve boot, and mints that workspace container-readable (0o755 — MkdirTemp's 0o700 is unreadable to the sandbox uid under rootless podman, per tools.EnsureWorkspaceDir's rationale). (b) fileOpRoot's forced-working-dir branch had no supporting-doc exception, so even a validated doc read was refused ("path escapes the scheduled-run worktree"). It now mirrors the conversation branch's narrow shape: the model's UNRESOLVED path must originate beneath the forced root and the RESOLVED target must land beneath a registered doc root. Read-only by construction — the doc-mount check at the top of fileOpRoot already refuses every writable op, and the sandbox anchors those roots read-only independently. (c) Nothing seeded the supporting-doc symlinks into scheduled/one-shot workspaces, so the system prompt's bare protocols/foo.yaml convention — which the audit enforcement itself relies on — silently broke. EnsureWorkspaceDir's seeding loop is extracted into tools.SeedSupportingDocSymlinks and called from configureRunWorkspace for every scheduled and one-shot run. The helper never plants a self-referential symlink (a non-worktree run seeds the shared workspace root, where the registered shared-file library dir IS <root>/shared). Writes into doc mounts, non-doc symlink escapes, absolute doc-mount paths, and ..-traversal out of the forced root all stay refused; regression tests pin each, plus the seeding idempotence/repoint/ real-file guarantees and the one-shot harness registration end to end. Closes #1290 Co-Authored-By: Claude <noreply@anthropic.com> --------- Signed-off-by: Brad <brad@elcanotek.com> Signed-off-by: jzhao234 <junzhao234@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: jzhao234 <junzhao234@gmail.com> Co-authored-by: Junhao Zhao <149023600+jzhao234@users.noreply.github.com> Co-authored-by: Roman Y <148697404+obsessixnv@users.noreply.github.com> Co-authored-by: Kristian Yendrek <122704517+KristianYe@users.noreply.github.com>
jzhao234
added a commit
that referenced
this pull request
Aug 28, 2026
#1336) * 📝 Point the Kubernetes docs at the example bundle for that path (#1259) * Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * 📝 Point the Kubernetes docs at the example bundle for that path fleet links to example-config in four places, but nothing referenced example-kubernetes-config — so an operator reading DEPLOYMENT-KUBERNETES.md had no pointer to a worked bundle for the path they were on, even though one now exists and carries the two Containerfiles, a documented values overlay for this chart, and an empty-cluster-to-working-fleet walkthrough. Four entry points, chosen because each is somewhere a reader lands cold: the Kubernetes guide's intro, the chart README (which renders a control plane but supplies no bundle for it to load), the two README spots where the template is introduced as one of the three ways in, and AGENTS.md's Kubernetes row. Framed as peers rather than parent and child, since that is what they are, and linked rather than vendored — client content stays out of this repo per the coupling doctrine. Docs only. Both URLs verified to resolve (HTTP 200; the repo is public). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> * 🚨 Stop the Connections focus assertions sampling a single tick (#1261) * Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * Promote dev → main: point the Kubernetes docs at the example bundle for that path (#1260) * Promote dev → main: drive every reclaimer and guard disk; approval-card rework; tracked web agent-rules files (#1217) Promote dev → main. Three changes since #1215: - Drive every reclaimer, and act on the disk you measure: one hourly in-process maintenance loop, one daily systemd timer for the podman image-store prune, and a disk guard that sheds background load asymmetrically (scheduler stops claiming; chat keeps serving). Plus a terminal backstop for unwakeable paused_awaiting_wake rows, session-cap enforcement in the idle reaper, the remote-MCP OAuth sweep folded into the ctx-bound loop, a single-pass ctx-aware /admin/storage walk, and one diskguard.Usage statfs implementation. - Rework approval cards: honest per-tool chrome, reload persistence, humane timeouts. - web: commit the agent-rules files next dev auto-generates. Full gate green: Go build/vet/lint/test, the -race lane, govulncheck, web lint/test/build, Playwright mocked and live, the Grype image scan, the migration DDL lint, gitleaks, and CodeQL. * Promote dev → main: enterprise security audit — three authorization holes closed, the CodeQL gate unblocked, docs corrected (#1248) Carries #1246 (the multi-scanner stack) and #1247 (the audit of what shipping it exposed). Three own-rows authorization holes closed: GET /tasks/paused leaked every principal's paused prompts and task UUIDs; PUT /tasks/{id} and POST /tasks/{id}/tags let any client-role principal rewrite a teammate's pending run including mcp_selection and credential_allowlist; /feedback and /learned-instructions acted on unowned tasks. All mutation-tested. The CodeQL gate now bands on security-severity >= 7.0 minus a per-file accepted-findings register with written reasons, replacing an any-finding threshold that had been armed on a zero measured during a diff-informed pull_request run and therefore deadlocked every push to dev and main. A vacuity check fails the job when findings resolve no rule metadata. Also: seven action refs repinned off annotated tag objects of mutable major tags, the fleet_ref GITHUB_OUTPUT newline-injection bypass, a docs-only classifier that skipped the whole suite on embedded product content, a redactor that held no connector secrets, a secret prefix in a validation error, dead code including an unenforced 24MB body-cap claim, and 39 false or stale doc claims. ADR-0048 records the gating decision; ADR-0036 and ADR-0012 amended. * Make ci.yml's pg_dump major assertion able to pass The assertion added in this range failed on its first real outing — got=16 against server 18 — over an install that had plainly succeeded (`Setting up postgresql-client-18 (18.6-1.pgdg24.04+2)`). Installing the package is not what decides the answer: /usr/bin/pg_dump is a symlink to postgresql-common's pg_wrapper, and the wrapper's own header states the rule — it calls the client "with the version, cluster and default database specified in ~/.postgresqlrc or /etc/postgresql-common/user_clusters". That is cluster configuration, not "the newest client installed", so on a runner image carrying a PostgreSQL 16 cluster, adding a client package alongside it changes nothing about the dispatch. Verified rather than reasoned about: this container has the same shape as the runner (/usr/bin/pg_dump -> ../share/postgresql-common/pg_wrapper, the versioned tree at /usr/lib/postgresql/16/bin), and there the wrapper likewise reports 16 while prepending the versioned bin dir resolves pg_dump to it directly. So the versioned bin dir goes first on $GITHUB_PATH. That is also what the test needs: internal/admincli/backup.go execs `pg_dump` off PATH, with no versioned path of its own, so PATH resolution — not package presence — is the thing that decides whether backup_test.go's major-mismatch t.Skipf turns the only coverage of `fleet backup`/`fleet restore` off. Asserted in two places, because $GITHUB_PATH takes effect only from the next step: the install by absolute path in the step that performs it, and PATH resolution in a step of its own. The second one is the assertion that mirrors what the test actually invokes, so the two cannot drift apart again without going red. The CHANGELOG entry claiming the step "now asserts the major" is corrected in the same commit — the assertion was there and could not pass, which is the kind of half-true the honesty-in-docs invariant is about. Verified: actionlint clean over every workflow; go test ./scripts/ passes, including TestPostgresMajorAgreesAcrossCI, which requires every postgres major named anywhere in .github/workflows to agree. Claude-Session: https://claude.ai/code/session_018eFXcF6BM7bxFVEQH67iuk * 📝 Point the Kubernetes docs at the example bundle for that path fleet links to example-config in four places, but nothing referenced example-kubernetes-config — so an operator reading DEPLOYMENT-KUBERNETES.md had no pointer to a worked bundle for the path they were on, even though one now exists and carries the two Containerfiles, a documented values overlay for this chart, and an empty-cluster-to-working-fleet walkthrough. Four entry points, chosen because each is somewhere a reader lands cold: the Kubernetes guide's intro, the chart README (which renders a control plane but supplies no bundle for it to load), the two README spots where the template is introduced as one of the three ways in, and AGENTS.md's Kubernetes row. Framed as peers rather than parent and child, since that is what they are, and linked rather than vendored — client content stays out of this repo per the coupling doctrine. Docs only. Both URLs verified to resolve (HTTP 200; the repo is public). Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> * 🚨 Stop the Connections focus assertions sampling a single tick One of these failed a docs-only PR yesterday, reporting document.activeElement as <body> where the API-key input was expected. Nothing in that diff could reach the web tier, and the same suite passed locally 1104/1104, so it was a flake — but a flake nobody had pinned down, and it would have bitten the next person just as arbitrarily. The mechanism, confirmed rather than guessed. The deep-link test awaits findByTestId("dir-form-browserbase") and then asserts focus inline. The form opening and the focus landing are not the same event: the state update comes from the catalog fetch resolving OUTSIDE act, so React schedules the card's apiKeyRef focus effect on a macrotask, while findByTestId resolves the instant the form NODE appears — a commit earlier. I proved the window is real by watching the raw DOM through a MutationObserver outside RTL's act wrapper: at the moment the form node appears, the input exists and activeElement is not yet it. On an unloaded machine the effect flush wins that race every time (0/25 repeats, and 0/12 under saturating CPU load, which is why it does not reproduce locally); on a loaded CI runner it can lose. So the assertion was never testing "focus ends up in the key field" — it was testing "focus has already landed at this particular instant", which is not a property the component promises or a user could perceive. Retrying it through waitFor tests the guarantee that actually matters. Two sibling assertions in the same file had the identical shape — dialog focus after findByRole, and the focus hand-back after the dialog unmounts. Neither has flaked yet; both could, for the same reason, so both are fixed now rather than after they do. Deliberately NOT changed: the focus assertions in Menu, Toast and admin/users. Those follow a synchronous fireEvent or a direct .focus(), which React flushes inside act, so they are not racy — waitFor there would be noise. The distinguishing factor is focus driven by an effect after an ASYNC state update. Kept its teeth, checked rather than assumed: removing the apiKeyRef focus() from the component still fails the deep-link assertion, and removing panelRef.focus() still fails the dialog one. 20/20 repeats green afterwards, plus npm run lint, typecheck and the full 1104-test suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M6WuXeSvxcwaFHTUoWKBVa --------- Signed-off-by: Brad <brad@elcanotek.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> * ⬆️ Bump the go-minor-patch group: bubbletea 2.0.9, fantasy 0.41.2 (#1254) Direct bumps: charm.land/bubbletea/v2 2.0.8 → 2.0.9 and charm.land/fantasy 0.41.1 → 0.41.2. Both are patch releases: bubbletea fixes MouseButton11 and media-record key mapping, a ProgressBarState.String() panic, a cursedRenderer pendingErase artifact and the kitty keyboard stack on exit; fantasy corrects Bedrock SSO auth priority and stops emitting empty reasoning_content fields on assistant tool-call messages. Transitive updates ride along: the AWS SDK v2 set, cloud.google.com/go/auth, anthropic-sdk-go, enterprise-certificate-proxy, google.golang.org/api, genai, genproto, protobuf 1.36.12 and testify 1.12.0. go.mod and go.sum only — no source changes. Verified on top of current dev: make build, make test (full suite against Postgres), make lint (Go + ruff) and make govulncheck all clean; govulncheck reports 0 vulnerabilities called. The PR branch was updated onto dev's head first, so the green Dev gate certifies this exact merge result. * agent,runner: extract RunTurn and executeTask into phase helpers TLDR: RunTurn (275 lines) and executeTask (305 lines) juggled enough state that two confirmed audit bugs (#1105, #1117) partly stemmed from them (#1127). Both now read as narratives of named phases -- RunTurn is 153 lines calling 7 helpers, executeTask is 180 calling 6 -- with every extracted body line-identical to its original span modulo the mechanical edits extraction forces (parameter threading, return plumbing, unindent). Zero logic changes, zero defer-scope changes. Fix: RunTurn gained admitInteractiveTurn, composeTurnSystemPrompt, assembleTurnMessages, interactiveRunSelection, openTurnRemoteOverlay, failedTurnResult, and completedTurnResult (mirroring the pre-existing cancelledTurnResult); executeTask gained buildTaskRunContext, captureRunFailure, parkForQuestion, finishStopped, finishLeaseLost, and finishSuccess (joining the existing per-outcome family). Function- exit defers stayed in the parents: the limiter release and overlay close are returned/re-registered by RunTurn under the exact HEAD conditions, and the sandbox/workspace + MCP-scope acquisition block was deliberately NOT extracted because it owns three such defers. finishSuccess's absorbed returns are equivalent because nothing follows the terminal switch; the terminal frame flows to the parent's deferred emit through the same map reference. Tests: zero test files touched -- the ADR-0035, #1116 lease/zombie, behavioral net. Equivalence proven mechanically: an AST inventory shows only the two parent functions' hashes changed (+13 helpers, all other declarations byte-identical), and three adversarial reviewers reproduced the inventory and hand-diffed all 13 extracted bodies against their HEAD spans (no undeclared differences) plus the full defer audit. Test rounds: both packages -count=1/-count=2/-race, agentcore/scheduledrun/httpapi/cmd-fleet dependents, full make test with both schemas -- green except the podman-gated sandbox integration tests (no fleet-sandbox image on this box; identical on dev). gofmt/vet/golangci-lint clean, no new suppressions. Signed-off-by: jzhao234 <junzhao234@gmail.com> * sched: encode the task lifecycle as a tested transition table TLDR: transition rules lived only in scattered WHERE status=... clauses across claim/recovery/serialization/reporting queries -- every new status touched them all by hand, and nothing enumerated the legal edges (#1127). The lifecycle is now a 49-edge tested constant (internal/sched/models/task_lifecycle.go) with derived status sets, init-time validation, and coupling tests that turn drift into red tests. No behavior change: the only runtime derivation is claim.go's already-named taskActiveStatuses now mapping the table-derived models.ActiveTaskStatuses (same {leased, running} set; the IN-list placeholder pin proves it). Fix: the table records every edge the GUARDED runtime writers can produce -- birth, due-sweep, run_if settlement, claim, worker report, retry requeue, dead-letter (in-process and #1116 recovery), pause/ask + expiry, wake/park + expiry + 24h backstop, cancel, DLQ replay, and the editable/replace re-derivations -- each edge naming its authoritative writer and file. Coupling is machine-checked at the strongest seam available per writer, honestly labeled: behavioral writer matrices drive each db/storage transition writer against a row in EVERY status and assert the outcome matches the table; an AST scan proves every status literal in the packages' tasks-table SQL is a known status; set pins cover cleanup, serialization placeholders, the scheduler's parametric callers, and the legacy-import births (admincli's validSchedTaskStatus === the table's birth To-set, both directions). Completeness validation (reachability, no non-terminal dead ends, terminal exits guarded-only) runs at package init and as a test. Deliberately OUTSIDE the model and documented as such: the two verbatim-upsert import paths (sched task import, legacy import --overwrite) can produce any->imported-status past every guard -- pre-existing restore surgery recorded, not changed. Findings encoded, not fixed (current reality preserved): cancel can erase a DLQ row's replayability (dead_lettered->cancelled is live); terminal refusal lists are inconsistent across writers; leased-> terminal edges are reachable because a failed running-report only logs; IsValidReportedStatus has zero production callers (the worker- report to-side is caller discipline, not a guard -- documented). Tests: red-cased three ways during review (edge removal fails the storage matrix; a guard change without a table edit fails the db matrix; a resurrected retired-status literal fails the scan); three adversarial reviewers independently re-inventoried every status- writing path and confirmed the guarded edges match reality exactly. Lifecycle tests -count=2; full sched tree + runner + admincli green; full make test green with both schemas except the podman-gated sandbox integration tests (no fleet-sandbox image on this box; identical on dev). gofmt/vet/golangci-lint clean. Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: unify the two-plane admin permission Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: put unified admin permission first Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: show viewer before member Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: call active roles contributors Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: assign permissions when creating users Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: describe permissions on hover Signed-off-by: jzhao234 <junzhao234@gmail.com> * admin: show role help immediately Signed-off-by: jzhao234 <junzhao234@gmail.com> * ci: check overrides against locked parents Signed-off-by: jzhao234 <junzhao234@gmail.com> * Anchor scheduled-run file staging to the worktree; stop confirm_audit aborts from failing finished work (#1280) download_url resolved a relative output_dir against the process cwd in scheduled runs, then refused its own path as escaping the worktree; runs with a forced working dir now also get a working-directory message tail so MCP file tools receive an absolute output_dir. Unbound same-tool re-audits supersede instead of stacking, an abort after all declared work executed is refused instead of flagging the run terminal, and an abort no longer requires the critical_actions unlock list. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * Support multiple logins ("seats") for hosted MCP connections (#988) (#1281) A user can hold several logins to one hosted MCP server — a work and a personal GitHub, two Gamma workspaces — each its own row with its own sealed credential and share grants, and choose which one a chat or a scheduled task uses. Mirrors the bundled <VAR>_<ACCOUNT> seat model. - Migration 051: remote_mcp_servers.account + is_default; uniqueness per (user, name, account); one default per name (partial unique index); every existing row becomes its name's default. - Migration 052: conversations.mcp_accounts — per-conversation seat override for the chat Tools picker (bundled connectors too). - Runtime: agent.RemoteMCPSelection (filter / pins / exact) replaces the enabled-set everywhere an overlay opens; exactly one seat per name is mounted, registered as RegisteredMCPName(name, account); a pinned seat that is not connected is skipped, never replaced by another account. - Broker protocol: RemoteScopeSpec gains accounts/exact (labels only). - Chat: overrides ride in MCPAccountDefaults; RunTurn rebinds the approval stager with the composite broker + mounted seats so cards record the seat that ran; approval execution against a hosted connection reopens that exact seat (remote half of #167 residual 2). - Tasks: mcp_selection may pin a hosted seat; hosted names route to the overlay instead of the bundle binder; unknown names still fail loudly. - API: POST /remote-mcp-servers accepts account; POST /{id}/default; PUT /{id}/account; pickers list one entry per name with accounts / default_account / account; POST /conversations/{id}/mcp-servers accepts accounts (unknown seat = 400); first POST /chat accepts mcp_accounts. - Web: Connections groups seats per name (Set default / Rename / Add another account); chat and task pickers gain a seat select. - Docs: docs/REMOTE-MCP-MULTI-LOGIN.md, ADR-0050, CHANGELOG. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * deps(go): bump github.com/go-chi/chi/v5 from 5.3.1 to 5.3.2 in the go-minor-patch group (#1277) Bumps the go-minor-patch group with 1 update: github.com/go-chi/chi/v5 5.3.1 -> 5.3.2. Signed-off-by: dependabot[bot] <support@github.com> * deps(web): bump next from 16.3.1 to 16.3.2 in /web in the npm-minor-patch group (#1278) Bumps the npm-minor-patch group in /web with 1 update: next 16.3.1 -> 16.3.2. Signed-off-by: dependabot[bot] <support@github.com> * Audit: an abort retires abandoned commitments; the confirm trailer names outstanding work (#1282) Field case (Energizer daily, 2026-08-25 16:11 UTC, task 9847380d): the audit declared the inline mcp_pages_update_page_data; the payload had gone by reference; mcp_pages_update_page_data_upload was BLOCKED as undeclared; the model aborted (correctly — nothing had executed), re-audited the upload tool and published v572. Finish enforcement still demanded the inline declaration, the model's only exit was a second abort, and a live page landed as error. - confirm_audit(success=false) now zeroes every declared-but-unexecuted commitment (typed and legacy) and drops blocked calls awaiting retry, and its response names what it retired. A later confirm_audit(success=true) already clears the terminal flag, so the run is judged on what executes after the re-audit; an abort AFTER that execution still hits the completed-work refusal from #1280. - The success trailer described the wrong ledger: a fresh declaration used to come back "All 0 critical actions executed. Finish now." It now names the outstanding call(s) to make, and counts actual executed critical calls. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * deps(web): bump @testing-library/user-event (#1284) Bumps the npm-minor-patch group in /web with 1 update: [@testing-library/user-event](https://github.com/testing-library/user-event). Updates `@testing-library/user-event` from 14.6.5 to 14.6.6 - [Release notes](https://github.com/testing-library/user-event/releases) - [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md) - [Commits](https://github.com/testing-library/user-event/compare/v14.6.5...v14.6.6) --- updated-dependencies: - dependency-name: "@testing-library/user-event" dependency-version: 14.6.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Shared files: native cross-chat file library (#1292) Admin-published files every conversation's agent can read, on both sandbox backends: canonical bytes host-side under DataDir, a read-only staged tree under the workspace root (nested :ro bind on podman, read-only subPath of the workspace claim on kubernetes), a self-healing reconciler, a per-turn prompt block, member list/download + admin manage API and Settings page, and the live shared_files_max_total_mb cap. Migration 053. Also fixes chat attachments under the kubernetes backend (staged into the conversation workspace at send time, since pods cannot see the uploads root), with the staging path defending its own path/log boundaries (CodeQL alerts 148/149 fixed in code). Design note: docs/SHARED-FILES.md * Guard user-influenced values in two RunTurn log lines (#1294) CodeQL's diff-informed run on the dev-to-main promotion flagged log-injection flows in RunTurn's re-auth notice (user-named remote MCP servers + caller email) and stream-failure line (user-selected model slug, error text, and the reason derived from it). All advisory, fixed anyway: every value passes a CR/LF-stripping guard — spelled with strings.ReplaceAll, the sanitizer go/log-injection actually models, so the alerts close instead of standing open as false positives — and the slug is %q-quoted. * health: make the /readyz sandbox probe backend-aware (#1287) Under FLEET_SANDBOX_BACKEND=kubernetes the sandbox readiness check ran `podman --version` on the control plane — a binary that shape never has — so every healthy install reported 207 degraded. The probe now reports on what sandboxes actually run on: one bounded (5s) apiserver GET /version through a new narrow KubernetesBackend.ApiserverVersion accessor, memoized by the generalized cachedProbe so the #215 unauthenticated-endpoint bound covers both backends. A missing backend handle reports an error — never ok, never a podman fallback. Podman path byte-for-byte unchanged. Refs #1264 (finding 2), #989. * validate-config: probe /api/v1/key so a bad API key actually fails (#1289) The model_api check GET'd OpenRouter's PUBLIC /api/v1/models, so any non-empty key was blessed with "API key authenticates" — a mis-created 64-hex junk secret passed validate-config and then 401'd on the first real completion. The check now probes GET /api/v1/key (401 on a bad key, 200 otherwise), converging with the provider probe's existing endpoint; warning-not-blocking posture, skips, base-URL override, and the never-print-the-key property unchanged. internal/fakellm serves /api/v1/key with the real auth contract so E2E seams don't 404. Refs #1264, #989. * sandbox: make FLEET_SANDBOX_WARM_SIZE=0 mean "no warm pool" (#1288) warmSize 0 was inexpressible: config defaulted the field to 0 and resolveWarmSize treated <=0 as "derive 2..8", so a kind-cluster overlay documenting "no warm pool" ran a two-pod Guaranteed-QoS pool anyway. The default becomes a -1 unset sentinel: unset derives (unchanged), 0 disables the warm pool, positive pins, below -1 fails loudly at load. Chart warmSize defaults null (omit env), emits any set value including 0, schema rejects negatives. Semantic change called out in the CHANGELOG. Review note for a follow-up: the negative-value guard sits as a post-load check instead of a min bound on the #1119 knob-registry row, so an explicit -1 is accepted as "derive" and ValidateEnvKnobs reports negatives as well-formed; min: bound(0) on the registry row would close both. Refs #1264 (finding 3), #989, #181. * docs: link email report onboarding runbook (#1286) Point AGENTS.md and CONNECTOR-ONBOARDING.md at the external canonical SES/S3 email-report onboarding runbook; client identifiers stay in the client bundle, per the engine/bundle coupling doctrine. * sandbox: ride client-go for kubernetes exec streaming (#1285) The hand-rolled v4.channel.k8s.io exec client lost stdin nondeterministically for multi-KB payloads on the first real cluster it met (#1264 kind rehearsal): the 28KB bridge upload wedged ~4 of 5 attempts and no tool call could run. Exec streaming now rides client-go's remotecommand WebSocket executor (v5, real stdin half-close), proven on the same cluster/pod/payloads. Adoption is deliberately narrow: client-go is a TRANSPORT for exec only — pod CRUD and the fail-closed preflight stay on the hand-rolled REST client, and the rest.Config is built from material fleet's strict kubeconfig parser already validated (clientcmd never invoked; exec plugins and insecure-skip-tls-verify still refused). ADR-0049 amended in place (its recorded revisit trigger fired). Hardened per review before merge: cappedBuffer is mutex-guarded and read via snapshot() copies (client-go abandons its copy goroutines on a cancelled stream — a reproduced -race failure on the cancelled-bash path); writeStdin is bounded so a stalled dial can never park the sandbox mutex forever; rest.Config pins Proxy off so exec never asymmetrically honors HTTPS_PROXY while pod CRUD ignores it. Regression tests pin all three. Refs #1264, #989. * ci: allow dispatching the full gate manually (#1295) pull_request webhook deliveries were dropped during the 2026-08-26 GitHub incident: the dev->main promotion PR's opened event was lost and five synchronize events never spawned a run, leaving the required "CI gate" check impossible to produce without empty commits or close/reopen — both forbidden. workflow_dispatch runs the identical suite on the branch head, so the name-matched required check is satisfied honestly; the docs-only classifier already treats un-diffable events as "run everything". * sandbox: seal the warm pool under lockdown and let sealed takes claim it (#1291) Under FLEET_DEFAULT_NETWORK_MODE=lockdown two defects shared one cause. Warm spawns (Pool.fill and Take's no-slot cold start) used PoolConfig.Container verbatim, so the pool parked OPEN-egress sandboxes (NoNetwork=false) — while every take under fleet-wide lockdown (the interactive lockdown branch, approved bash, and scheduled runs) forced a sealed cold start via TakeContainer. The result: N warm sandboxes of dead reserved cpu/memory/disk that nothing could ever claim, respawned forever by the TTL keeper, plus a cold-start latency tax on every lockdown turn. On the kubernetes backend the parked pods were labeled fleet.elcanotek.com/egress=open directly beside a boot log line claiming every pod is labeled none. The fix makes the warm pool useful under lockdown instead of deleting it: - warmContainerConfig seals warm spawns (NoNetwork=true) when the pool's fleet-wide mode is lockdown on a container backend. This alone makes the k8s egress=none label claim true by construction and the podman warm containers sealed. ModeHost is excluded — no network namespace to seal, and host pools never reach the container takes. - TakeContainerWithOverrides routes a sealed, zero-override take on a lockdown pool to Take(): the parked sandbox already has the exact posture and pool-default ceilings the caller wants, and Take's own no-slot fallback cold-starts through warmContainerConfig, so the sealed posture holds warm or cold. Every other combination keeps today's behavior exactly: any resource override cold-starts (per-task ceilings need a fresh container), a sealed take on a NON-lockdown pool still cold-starts sealed (that warm inventory is open, and a sealed take must never receive an open sandbox), and allowlisted takes are untouched (the per-turn proxy token requires a cold start). Caller audit for the posture invariant: under fleet-wide lockdown no open take of the warm pool exists — takeTurnSandboxFrom, takeTaskSandbox, and takeStagedBashSandbox all route lockdown to TakeContainer and reach plain Take() only on ErrContainerUnavailable (an image-less host/mock pool, where there is no network namespace to seal) or in non-lockdown modes (where warm spawns stay open). TakePersistent is unreachable under fleet-wide lockdown (the lockdown branch precedes the persistent borrow in every caller). Doc truth restored alongside: the "always cold-starts" comments on TakeContainer / TakeContainerWithOverrides, the container.go :z SELinux comment's lockdown sentence (the shared-label rationale is unaffected; the behavioral claim was about to become a lie), the kubernetes boot log line that lied before, and the sandbox-probe / scheduledrun taker comments. Tests pin all four behaviors against the fake kubernetes apiserver (pod egress labels + pod identity distinguish warm claim from cold start) and the podman-shared warmContainerConfig seam: lockdown warm spawns are sealed, lockdown TakeContainer claims a warm sandbox, an open pool's sealed take still cold-starts sealed, and overrides still cold-start under lockdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GhoJPvJfQGVoucCyczmEkR * tools/taskrun/scheduledrun: let scheduled and one-shot runs use the file tools and read bundle docs (#1290) Three stacked, backend-independent gaps made every scheduled run's and fleet task run's file-tool calls fail — workspace writes in the one-shot harness, and view_file protocols/... everywhere: (a) internal/taskrun never registered the workspace root or the supporting-doc dirs, so tools.ValidatePath fell back to its legacy process-cwd allowlist (/opt/fleet/client in the split control-plane image) and rejected every workspace-relative path. taskrun now registers both globals with its minted workspace, exactly like the serve boot, and mints that workspace container-readable (0o755 — MkdirTemp's 0o700 is unreadable to the sandbox uid under rootless podman, per tools.EnsureWorkspaceDir's rationale). (b) fileOpRoot's forced-working-dir branch had no supporting-doc exception, so even a validated doc read was refused ("path escapes the scheduled-run worktree"). It now mirrors the conversation branch's narrow shape: the model's UNRESOLVED path must originate beneath the forced root and the RESOLVED target must land beneath a registered doc root. Read-only by construction — the doc-mount check at the top of fileOpRoot already refuses every writable op, and the sandbox anchors those roots read-only independently. (c) Nothing seeded the supporting-doc symlinks into scheduled/one-shot workspaces, so the system prompt's bare protocols/foo.yaml convention — which the audit enforcement itself relies on — silently broke. EnsureWorkspaceDir's seeding loop is extracted into tools.SeedSupportingDocSymlinks and called from configureRunWorkspace for every scheduled and one-shot run. The helper never plants a self-referential symlink (a non-worktree run seeds the shared workspace root, where the registered shared-file library dir IS <root>/shared). Writes into doc mounts, non-doc symlink escapes, absolute doc-mount paths, and ..-traversal out of the forced root all stay refused; regression tests pin each, plus the seeding idempotence/repoint/ real-file guarantees and the one-shot harness registration end to end. Closes #1290 Co-Authored-By: Claude <noreply@anthropic.com> * Close out the #1298–#1302 batch: promotion ancestry automation, gate-cancellation neutrality, warm-size bound, node-LTS reminder, scheduled shared-files announcement (#1303) Five fixes, one per issue: - #1298: Promotion ancestry workflow — on push to main, verify main^{tree} == dev^{tree}, record git merge -s ours on dev; manual fallback documented in CONTRIBUTING.md ("Promotions"). - #1299: FLEET_SANDBOX_WARM_SIZE carries min: 0 in the knob registry; every explicit negative (including -1) is rejected at the validation seam. - #1300: node-lts-reminder.yml files the node-26 move issue (full checklist) once v26 is past its scheduled LTS date while web/.nvmrc is behind. - #1301: shared file library announced to scheduled runs via the one renderer (sharedfiles.PromptBlock); fleet task run stays out of scope; docs/SHARED-FILES.md updated. - #1302: Dev gate / CodeQL gate / CI gate conclude neutral over cancelled (superseded) needs; a real failure still turns them red. * test(agent): assert the remote-MCP overlay closes at RunTurn exit (#1306) Adds TestManagerRunTurn_ClosesRemoteOverlayOnEveryExitPath: injects an opener through the existing ManagerOptions.OpenRemoteMCPOverlay seam, returning an active broker-backed overlay whose CloseScope counts releases, then drives full turns through the mock-mode/fake-LLM fixture for each way a turn can end (success, fatal failure, cancellation) and asserts Close fired exactly once by the time RunTurn returned. Two ordering probes pin the release to RunTurn's exit rather than "sometime": the overlay must be open when handed to the turn and still open at CommitTerminal. Verified by deleting RunTurn's overlay-close defer — all three subtests fail — then restoring it. Test-only; no production file touched. Closes #1275 * chore: ignore agent-harness git worktree checkouts (#1307) Parallel AI-agent work creates one full `git worktree` checkout per agent under .claude/worktrees/. Untracked they surface as pending changes in every status/pre-push check; committed they would land the repository inside itself. Ignore only that directory, so the rest of .claude/ (skills, settings — referenced by AGENTS.md) stays tracked. Same reasoning as the web/node_modules entry above it. * sched: one terminal refusal set, and refuse cancelling a dead-lettered task (#1310) All four internal/sched/storage transition writers now guard the from-side on TaskStatus.IsTerminal() — the one set models.TerminalTaskStatuses mirrors and validateTaskLifecycle cross-checks at init — instead of hand-listing three or four statuses apiece. The refusal style stays split and is now documented as a decision: cancel errors (an operator request), the three lease-guarded runner writers return the row unchanged (a late idempotent report must not fail a run that already landed). Cancelling a dead_lettered task is now refused. A DLQ'd occurrence exists to await operator replay, and cancel moved it to a status no replay path leaves, silently destroying that. No caller depended on the old edge: web STOPPABLE_STATUSES already excludes dead_lettered, chat manage_tasks pre-skips terminal rows, and there is no CLI cancel — so no UI/API/CLI change was needed. The error keeps the "cannot cancel" substring the HTTP handler maps to 400 and names the two real options: replay or delete. The worker-report to-side is now enforced rather than left dead: IsValidReportedStatus guards UpdateTaskStatusAtomicWithContext, refusing with ErrTaskNotReportableStatus before BeginTx, since a bad target is a caller bug and not a race. No behavior change for any edge in the current table: the three runner writers' widening is inert because a dead_lettered row holds no lease, so their lease check refuses first. Only cancel's guard changed observably. Closes #1268 Closes #1269 * deps(web): bump the npm-minor-patch group in /web with 2 updates (#1305) Bumps the npm-minor-patch group in /web with 2 updates: [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) and [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react). Updates `@types/react-dom` from 19.2.4 to 19.2.5 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom) Updates `@vitejs/plugin-react` from 6.0.5 to 6.1.0 - [Release notes](https://github.com/vitejs/vite-plugin-react/releases) - [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.1.0/packages/plugin-react) --- updated-dependencies: - dependency-name: "@types/react-dom" dependency-version: 19.2.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: npm-minor-patch - dependency-name: "@vitejs/plugin-react" dependency-version: 6.1.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: npm-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bound the runtime-secret literal set and cover the control plane (#1311) Two follow-ups from #1124. Bounded literals via a generation/epoch swap rather than FIFO: rotation is the moment fleet knows a secret was superseded, so no per-server cap has to be guessed and the only tunable is the window. internal/redact now separates PERMANENT literals (AddLiteral — boot env secrets, static api_keys) from SCOPED ones keyed by hosted-MCP server row. AddScopedLiterals joins a row's current generation; RotateScopedLiterals opens a new generation with the row's complete live set and starts retirement on the previous one. literalRetireGrace = 15m, with maxScopeGenerations = 4 as a refresh-storm backstop. Retirement can only ever drop a value the same scope superseded, re-listed values are revived, and permanent literals are never demoted. Steady state is 3 literals per connection; sweeps are lazy behind an atomic pre-check. Bytes are not zeroed because Go strings are immutable — retirement drops the last reference for the GC, as the code states. Coverage extended past the broker child through one scope-aware observer seam, func(scope string, rotated bool, secrets ...string), wired to agentcore.RegisterSecretLiterals and mcpbroker.RegisterSecretLiterals. Main-process control-plane acquisitions now register before the request that could echo them: callback code exchange (including the single-use authorization code), Authorize's unsealed client secret, the DCR secret and registration access token, and both api_key probes. httpapi.remoteMCPError's default branch, which relays wrapped vendor text, now runs through agentcore.RedactSecrets so those literals reach the redactor that sees that string. Closes #1274 * Gate Optional variant seats through one server-name keying rule (#1308) The two layers deciding whether an Optional MCP server's tools are available keyed their opt-in checks differently. agentcore's Gate-1 did an exact map lookup on the registered server name, while the system-prompt roster prefix-matched tool names and resolved the longest matching Optional server. For a named-account variant seat jira_prod whose bundle declares only jira as optional, Gate-1's exact lookup missed: the seat's tools registered and were CALLABLE on every run while the roster hid them from the model — the dangerous half of the mismatch, since an operator believes the connector is opt-in gated when it is not. Both layers now resolve through one helper, longestServerKey, implementing a single documented rule: a key K governs a name N iff N == K or N begins with K_, longest key wins. Gate-2's mcpAllowlist.toolsFor, which already implemented this rule by hand, is refolded onto the same helper so the three gates cannot drift apart again. Gate-1 now fails closed on a variant seat rather than the roster being loosened to match the leak. The whole-name branch stays disabled for roster names: a roster name always carries a trailing tool segment, so mcp_jira_search must resolve to server jira, never to a server literally named jira_search. This preserves the roster's existing behavior byte-for-byte and leaves the prompt-cache prefix goldens untouched. Closes #1272 * Persist a round-capped scheduled run's partial transcript (#1309) When a scheduled run exhausts the enforcement rounds without its finish gates clearing, agentcore.Run returns the accumulated Result alongside the error (#1125) — but the scheduled driver's `if err != nil { return err }` ran before its FinalText persistence block, so up to 20 rounds of paid assistant text never reached the session log. #1125 made that accounting available; this surfaces it. A new agentcore.ErrMaxEnforcementRounds sentinel wraps the round-cap error so the driver recognizes the case structurally via errors.Is rather than by string-matching. The rendered message is byte-identical, so operator log greps and the existing strings.Contains assertion still hold. internal/agent/scheduled.go's persistRoundCapPartial then writes two records into the session log — a [truncated] notice naming the rounds burned plus the prompt/completion tokens and dollars spent, then the carried FinalText — both stamped message_type round_cap_truncated, before the error is returned. The run still FAILS identically: same message, same terminal failure class (the sentinel is deliberately not added to runner.classifyFailure, asserted by a new TestClassifyFailure case), same retries, same notifications. Only transcript visibility changed. The other halves were never missing — tool calls, results and enforcement nudges are written live by scheduledObserver, and the token/cost counters live by agentcore's orchestration accounting into the same LogSession. Closes #1271 * Validate task imports at the seam: no resurrection, no lease overwrite; key provenance immutable (#1312) db.AddTask is an unconditional full-column upsert whose ON CONFLICT DO UPDATE includes status, lease_owner and lease_expires_at, and two operator import paths reached it against existing rows with no status validation. Every transition guard in the system sits above that seam, so the upsert bypassed them all: re-importing an envelope for a task that had run to success rewrote it back to scheduled with a stale scheduled_for and the due sweep re-ran its external side effects, and importing over a running row overwrote the live lease mid-run. Validation now lives at the import seam, not in AddTask, whose verbatim semantics are load-bearing for same-generation re-import idempotency. New internal/admincli/import_policy.go is called by both paths: - importableTaskStatus (renamed from validSchedTaskStatus) now gates the sched task import envelope too, so no import can write leased, running or either paused status, nor an unknown/retired/empty one. - A status collision on an existing row is refused without an explicit opt-in: new `fleet sched task import --replace-status`; the legacy importer's opt-in is its pre-existing --overwrite. - Neither opt-in can touch a lease: a write over a leased/running row is refused outright, and the lease columns are never importable onto an existing row. The envelope path checks the whole batch before any write. Refusal was chosen over "definition-only otherwise": a partial overlay would have to freeze status plus the run-outcome columns with no machine-checkable coupling to the registry, and its failure mode is a silently incoherent row. created_by_key_id also leaves the upsert set — provenance is immutable creation-time data, with no legitimate re-stamp anywhere in the tree — and both exclusion reasons now state that intent rather than "historical asymmetry". The #1126 round-trip test pins it. Closes #1267 Closes #1270 * Extend env-knob strictness to the knobs parsed outside the config loader (#1313) #1119's fail-loud registry covered every knob config.Load reads; a handful parsed elsewhere still silently (or only warn-)defaulted, and validate-config could not preflight them. Every out-of-loader knob is now a row in the one envKnobs registry under a third class, scopeExternal. config.Load does not consume their values but validates them, folding failures into the same one-pass boot error with each message naming the reading package, and ValidateEnvKnobs walks the whole table so validate-config preflights every knob the binary parses anywhere. Two reads left the ad-hoc path entirely for the new exported config.EnvKnobInt: fleet serve's rate-limit trio (replacing envIntDefault's warn-and-default) and fleet backup's retention, a verb that never calls Load. Strict bounds are exactly what each consumer already accepts, so nothing honored today starts being refused. Five bools became a new kindStrconvBool because their reader is strconv.ParseBool — registering them as plain kindBool would have certified `=on`, which that reader resolves to false. FLEET_OTEL_SAMPLE_RATIO is the only documented-lenient knob: it carries a required rationale and renders as a non-blocking warn. The sweep found 17 knobs the issue did not list, all folded in. Also fixed because the promise depends on it: 11 of these were missing from the .env allowlist, so a FLEET_ENV_FILE-only value was dropped before either reader or gate saw it — including FLEET_DISABLE_PROMPT_CACHE, documented as an env-file knob all along. New knobs_sweep_test.go is a repo-wide AST sweep that finds ad-hoc os.Getenv reads whose value flows into a strconv/time parse, plus package-local env-parse helpers and their call sites, and fails until each key is registered or exempted with a reason. Closes #1273 * sched: return the lease sentinel from every lease-guarded writer (#1314) internal/sched/storage has exactly three lease-possession guards, but only UpdateTaskStatusAtomicWithContext returned ErrTaskLeaseNotHeld. RequeueTaskForRetryWithContext and DeadLetterTaskWithContext built their refusal with fmt.Errorf over the same string, so errors.Is failed on those two paths while working on the third. This is a trap rather than a live misbehavior: the runner already branches on this identity in two places — renewActiveLeases cancels a zombie run's context so its side effects stop once a renewal proves the lease was recovered, and the success-commit path suppresses side effects on a fenced write — and anyone extending that to the retry or dead-letter paths would have read a false negative off an error whose text stated exactly what happened. Neither current call site does an identity check on those two paths, so there is no behavior change today. Both now return the sentinel unwrapped, on purpose, so err.Error() stays byte-identical: only the identity changed. The sentinel's doc comment names all three writers and says a new lease guard must return it, not its text. A sweep of internal/sched found no other instance of the anti-pattern: every other sentinel is returned or %w-wrapped at every construction site, and the identical-looking literals in handlers/notes.go, handlers/prompts.go and admincli/sched_dlq.go are HTTP/CLI presentation text emitted after an errors.Is check. Bare errors.New one-offs with no corresponding sentinel and no caller matching on them were left alone. Found while working #1268/#1269 (PR #1310) and fixed directly rather than filed. * Borrow the high-value Prime Agent ideas: structured/iterative compaction, plan re-announcement, budget wind-down, completion-audit wording (#1317) A real diff of fleet against PrimeIntellect-ai/prime-agent (#990), with the four ideas that cleared the high-value bar ported behind fleet's existing governance: structured + iterative compaction summary prompts, post-compaction task-plan re-announcement, the FLEET_BUDGET_WINDDOWN_FRACTION soft budget wind-down notice, and the completion-audit wording in the scheduled self-audit nudge. docs/PRIME-AGENT-COMPARISON.md records the comparison and the deliberate non-borrowings with reasons. * web: ship Nebula Sans + Hack, and tabular figures with them (#1324) Elcano standardised on exactly two typefaces: Nebula Sans (SIL OFL 1.1) for UI/body/headings and Hack (MIT + Bitstream Vera) for code, logs and tabular output. Removes the four self-hosted IBM Plex woff2 files and the next/font/local wrapper; adds one vendored sheet (a copy of flag's design-system/fonts/fonts.css) that is now the only place a font family is named, with both licence files shipping beside the binaries. Still self-host-only — no font CDN. Nebula Sans has PROPORTIONAL figures (digit advances 407-625/1000) where IBM Plex Sans had every digit at 600, so tabular figures are now the @layer base default for every table plus explicit on the non-table numeric readouts. Measured: an 8-digit cell's all-1s vs all-8s width differed by 25.09px before and 0.11px after. web/src/app/fonts.test.ts guards the two-face rule, the licence files, the url() paths and the tabular-figures rules; the docs screenshots are regenerated. * fix(web): honor default-on connectors in Ops tasks (#1334) TLDR: New Ops tasks now start with catalog connectors marked enabled by default, while existing tasks preserve their saved connector selection. Problem: - Selecting optional connectors could hide default-on mailbox connectors from Ops runs. - New-task defaults loaded asynchronously and were not reflected in the picker. Fix: - Derive untouched new-task selections from enabled non-remote catalog entries. - Preserve explicit operator choices and persisted selections on existing tasks. - Document connector-selection behavior and compatibility semantics. Tests: - npm test -- --run web/src/app/orchestrator/TaskCreateModal.test.tsx - npm test - npm run lint - npm run typecheck - npm run build - git diff --check * fix(ops): preserve connectors on task resubmit (#1335) TLDR: Connector edits made while resubmitting a completed Ops task now reach the new one-off run instead of silently inheriting the source selection. Problem: - The terminal-task editor displayed connector changes but omitted mcp_selection from rerun overrides. - The rerun API did not accept a connector-selection override. Fix: - Add nil-aware mcp_selection rerun overrides so omitted inherits and explicit empty clears. - Send the editor’s complete visible connector selection on terminal resubmits. - Document the immutable-source/new-copy behavior. Tests: - go test ./internal/sched/handlers - npm test -- --run src/app/orchestrator/TaskCreateModal.test.tsx - git diff --check --------- Signed-off-by: Brad <brad@elcanotek.com> Signed-off-by: jzhao234 <junzhao234@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Brad Flaugher <16511019+bradflaugher@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Brad <brad@elcanotek.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Roman Y <148697404+obsessixnv@users.noreply.github.com> Co-authored-by: Kristian Yendrek <122704517+KristianYe@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Started as "restore the broken CodeQL setup"; finished as a blocking, agent-readable scanning stack where every finding was fixed — none deferred, none silenced without a written, mutation-tested reason — and everything gates through the checks that already gate (
ci-gateonmain,Dev gateondev), with no branch-protection change.Verified end-to-end in CI: Dev CI run 525 (
32580031374) is green on this head with the full stack live — CodeQLsecurity-extendedon all four languages, Semgrep's four registry packs, ruff check + format, npm audit + override canary, all blocking.Design notes:
docs/SCANNING.md(the stack: who checks what and why) anddocs/CODEQL.md(why default setup died, how Go extraction is proven, why quality queries moved to ruff).What was broken
CodeQL default setup was OFF — nothing scanned this repo. When enabled it died on Go:
go.mod requires go >= 1.27.0 (running go 1.26.6; GOTOOLCHAIN=local). The failure sat behind a red-but-not-required check for weeks, which is the rot pattern several pieces of this PR exist to prevent recurring.The stack that ships
security-extended, 4 languages)ci-gate/Dev gate(workflow_call)p/github-actionsp/golangp/javascriptp/python, pinned 1.174.0)ci-gate/Dev gate(workflow_call)E4,E7,E9,F,B,SIM,S) +ruff format --checkci-gate/Dev gate+make lintnpm audit --audit-level=low(web + rampart) + override staleness canaryci-gate/Dev gateBoth scanners fail their job on any finding and print findings (with
file:line) plus coverage counts (files in the database, files scanned/skipped, parse errors) to the job log and step summary — so a green check means clean, not ran, and an agent can fix from the log without the Security tab. SARIF still uploads to the Security tab; Semgrep JSON uploads as an artifact.Gating mechanism (corrects an earlier claim in this PR that a settings click was needed):
codeql.ymlandsemgrep.ymlare reusable workflows (on: workflow_call) called as jobs byci.yml/dev-ci.yml, so they sit in the gates'needslike any other job.Every finding fixed
actions/setup-gowithgo-version-file: go.mod(never a literal — the existing directory-wide test enforces this, verified by mutation),GOFLAGS: -tags=fleet_host_executorsohost.gois in the DB. Extractor output + DB file counts checked, not just a green check.p/github-actions), Dependabot-compatible form.scripts/rampart-service(which had no lockfile at all):sharplibvips CVEs +adm-zipGHSA viaonnxruntime-node. No upstream fix exists, sooverridesforce patched lines — installed and load-tested (sharp renders through the new libvips, transformers loads, adm-zip round-trips).scripts/check-npm-overrides.shfails the build the day upstream makes an override droppable.zip(strict=True),NamedTemporaryFileinto itswith, best-efforttry/except-pass→ explicitcontextlib.suppress, one reasoned# noqa: S603. Whole tree ruff-formatted in a dedicated commit, validated by the full Go suite.security-extendedfound exactly one thing — and it was real:actions/untrusted-checkout/mediumonbuild-sandbox-image.yml'sfleet_ref-fed checkout. Fixed, not waived (theactionslanguage has noAlertSuppression.ql): the workflow now refusesrefs/pull/*refs before checkout (fork-controlled code would otherwise run via the checked-out build script), and the identical hardening went intopublish-sandbox-image.yml— the unflagged twin holdingpackages: writethat the name-heuristic query missed.nosemgrep: <rule-id>+ reason (three already gosec-triaged; one —insecure-file-permissionsadvising world-readable0o644for a sandbox dir — is actively wrong). Every waiver mutation-tested: strip it and the finding returns.${{ }}interpolated intorun:blocks — also the injection-safe fix.Deliberate decisions (with receipts in the docs)
workflow_runwatcher (scan-cron-alarm.yml) because a called workflow may not request permissions its caller didn't grant — checked at plan time; learned by startup-failing an entire Dev CI run with the in-job variant.analysis-kindsis GitHub-internal — it errors and exits 0 in advanced setup. Documented so nobody re-adds it.upload-sarif@v4calls (grype, govulncheck) untouched per the brief, beyond SHA-pinning.Coverage gaps + items only the repo owner can close
_test.gofiles (621) are outside CodeQL's database (autobuild builds packages, not tests) — unchanged from default setup..github/workflows/;devbranch-protection state unknown to me; no SBOM/signing/provenance. Named indocs/SCANNING.mdrather than left for an auditor to find.Validation
make build/make test(foreground,-p 1) /make lint(golangci-lint v2.13.1 + ruff + migration DDL lint) green locally at each step.npm ci, oxlint,tsc, vitest (1104), build green.