Skip to content

ci(deps): bump aquasecurity/trivy-action from a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 to ed142fd0673e97e23eac54620cfb913e5ce36c25 - #200

Closed
dependabot[bot] wants to merge 47 commits into
mainfrom
dependabot/github_actions/aquasecurity/trivy-action-ed142fd0673e97e23eac54620cfb913e5ce36c25
Closed

ci(deps): bump aquasecurity/trivy-action from a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 to ed142fd0673e97e23eac54620cfb913e5ce36c25#200
dependabot[bot] wants to merge 47 commits into
mainfrom
dependabot/github_actions/aquasecurity/trivy-action-ed142fd0673e97e23eac54620cfb913e5ce36c25

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Aug 24, 2026

Copy link
Copy Markdown

Bumps aquasecurity/trivy-action from a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 to ed142fd0673e97e23eac54620cfb913e5ce36c25.

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

vishr and others added 30 commits August 4, 2026 11:07
Fanout: single-binary OpenTelemetry investigation. One Go process owns OTLP
ingest, DuckLake/Parquet storage, DuckDB queries, MCP, the agent runtime, and
the embedded React client.

Licensed under AGPL-3.0.
Build ordering was previously prose in the README: the browser workspaces
write into internal/ui/dist and internal/mcp/apps, both committed and
compiled in via go:embed, so the binary is only as fresh as the last UI
build. The justfile encodes that ordering, and `just check` is the single
gate that lefthook's pre-push hook and CI both run.

`just ui-check` guards the embedded assets against drifting from their
source. It builds both workspaces and byte-compares the result against the
committed bytes, using git as its backup: it refuses to run when those
paths are already dirty, and always restores them, so it never mutates the
working tree. This is the check that was missing when the assets last went
stale.

Hooks are split by cost — formatting and linting on commit, the full gate
on push, where the UI rebuild and test suite are worth their minute.

Action and tool versions are pinned exactly rather than floating. A bun
upgrade can change bundle bytes, which would trip the drift check from a
CI run rather than a deliberate commit.

openspec/ ships scaffolded but empty; `just spec-check` validates it and
currently passes vacuously until specs land.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A bare `testdata/` rule ignored every Go package's fixture directory, not
just a root-level scratch dir. internal/brand's test reads a vendored
testdata/favicon.svg, so the file existed on the machine that wrote the
test but was never committed, and `go test ./...` failed on any fresh
clone. Local runs passed only because the untracked file was present.

Scope the rule to /testdata/ and commit the fixture. Go treats testdata/
as source, so package fixtures must be tracked.

Found by the first CI run on a clean checkout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The README read like an internal note: a bare layout table, three manual
build commands, and no statement of what Fanout is or why the single-binary
shape matters. Replace it with a structure a first-time visitor can act on —
what it does, how it fits together, how to run it, and how to develop on it.

Add the runtime architecture and persistence diagrams, authored in d2 and
rendered to committed SVG, plus a `diagrams` recipe to regenerate them. The
SVG is committed so reading the repo never requires d2.

Document the configuration honestly: Fanout does not start unconfigured.
Local auth requires SMTP and a 32-character AUTH_CODE_SECRET, OIDC requires
a provider, and the agent requires AI_API_KEY. The quick start lists the six
variables that were verified to boot the server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The repository claimed to be "opened up incrementally" while omitting the one
exclusion that actually cost users: there was no Dockerfile, so a self-hosted
observability tool had no deployment path at all. Restore the multi-stage
build and publish ghcr.io/labstack/fanout from CI on main and on tags.

Harden the image over the version it came from: it runs unprivileged as a
system user, carries OCI labels linking it back to this repository, and has a
HEALTHCHECK. The healthcheck uses GET rather than wget --spider, because
/healthz rejects HEAD with 405 and a spider probe reports a healthy container
as unhealthy — confirmed by running it.

Releases are CalVer, v{YYYY.MM}.{N}. The metadata action matches the tag
literally rather than parsing it as semver, which mangles a leading zero like
v2026.04.1.

Add SECURITY.md and CONTRIBUTING.md; the README invited contributions with no
disclosure path and no setup guidance.

Replace the Status section with Scope. A fresh clone builds a complete working
binary, so "opened up incrementally" misdescribed the repository: what is
withheld is LabStack's operation of Fanout, not part of Fanout.

Drop dead configuration: golangci-lint exclusions for two files that do not
exist, .gitignore entries for site/, traefik/, and binaries with no packages.
Narrow the blanket *.png rule to the root for the same reason testdata/ was
narrowed — it would silently swallow committed image assets. Repoint four
comments that referenced site/ and scripts/ at files this repository has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fanout is optimizing for adoption rather than for preventing third parties
from hosting it, and AGPL costs adoption: several large companies ban it
outright, so a tool meant to run inside their infrastructure never gets
evaluated. Apache-2.0 removes that barrier.

Relicensing is clean right now and will not stay that way. Every commit is
authored by v@labstack.com and there are no external contributors, so
LabStack owns all of it. Add a CLA before that changes: it grants the right
to relicense, which is the only thing that keeps future licensing decisions
open. Without it, Apache-2.0 is a one-way door, since altering terms later
would need permission from every past contributor and one unreachable person
could block it permanently.

Nothing in the dependency tree constrained the choice. All 62 modules linked
into the binary are MIT, BSD, Apache-2.0, or MPL-2.0, with no GPL, LGPL, or
AGPL anywhere.

Signatures are stored on an orphan cla-signatures branch rather than main, so
signing does not add bot commits to release history or trigger an image
publish. The workflow never checks out pull request code, because
pull_request_target runs with a writable token in the base repository.

Add NOTICE for attribution, as is conventional under Apache-2.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The README claimed Fanout replaces a fleet of services with one process and
offered no evidence of what that process sustains. Measuring it exposed that
cmd/bench could not answer the question: it took a fixed -rate and -workers,
and the adaptivity that sized them lived in scripts/, which is not part of
this repository. Hand-picking a rate measures the guess.

cmd/bench now sizes senders from the driver's cores, ramps until the server
stops keeping up, bisects to find the boundary, and confirms at that rate.
Three details were forced by measurement rather than designed up front:

  - Bisecting, not stopping at the first miss. A doubling ramp that halts on
    one noisy step reported 1991/s and 3946/s for the same machine minutes
    apart.
  - A 0.95 delivery floor. At 0.90, a step delivering 90.8% was called
    sustainable at 7267/s and the confirmation held 6503/s.
  - Reporting the lower of ramp estimate and confirmed rate. Capacity is not
    stationary: rollup cost grows with the dataset, so a ramp can measure a
    machine whose capacity is falling underneath it.

Remove the built-in 1500ms query SLO. A fixed millisecond threshold encodes
the machine it was calibrated on, so the same binary reports a defect on two
cores where there is only a smaller computer. Every integrity failure stays:
dropped rows, a mid-run restart, a lost baseline, and an excessive send-error
rate still fail hard, because those make the numbers untrustworthy rather
than merely unflattering.

Results on two dedicated vCPUs: 5,138 traces/s (22,612 rows/s), export p95
3ms, zero dropped. Five queries/s costs 47% of that ingest capacity. The
report also records what the runs exposed operationally — an OOM kill at
7.5GB RSS with DUCKDB_MEMORY unset, and backfill sustaining 832 traces/s
against 5,138 while producing 9.37GB on disk for ~660k rows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lefthook skips pre-push jobs when its changed-file detection returns an
empty set, which it did on some pushes: the gate reported 0.03s and ran
nothing, while other pushes ran the full 13s. An intermittently-enforced
gate is worse than an honest one, because a green push stops meaning
anything.

Document it as the local convenience it is. CI runs the same just check
unconditionally and is what actually enforces the gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fanout's default configuration could be killed by the kernel. On a 2 vCPU /
8 GB host the process was OOM-killed at 7.5 GB RSS:

  Out of memory: Killed process (fanout) anon-rss:7566436kB

DUCKDB_MEMORY was unset by default, leaving DuckDB's own self-sizing in
place: 80% of detected memory, about 6.2 GB there. That budget is computed
as though DuckDB owned the machine, but it is a library inside a Go process
whose heap, stacks, and GC headroom sit on top. 80% plus the runtime exceeds
100% and the kernel settles the difference. The README's Docker quick start
reproduces this on any host where that sum exceeds the box.

Resolve it instead from detected memory, reserving 40% for the runtime, and
detect cgroup-first because a container limit is what the kernel actually
enforces — reading the host's total from inside a constrained container is
the same class of mistake. A --memory 6g container reports 6442450944 and
now resolves to 3686MB, against the 5.2 GB that failed.

When no source is conclusive, leave the value empty and warn rather than
guess: capping an unknown machine trades a hypothetical crash for a certain
slowdown.

DUCKDB_MAX_CONNS was fixed at 4 regardless of the machine — four concurrent
queries oversubscribing two cores, and an artificial ceiling on thirty-two.
It now scales with cores, floored at 2 because internal/lake rejects a pool
above 1 without a write gate, and capped at 16 where connections stop adding
concurrency.

FLUSH_BATCH_SIZE is deliberately untouched. The appender benchmark shows
batch size matters, but not how it trades against Parquet file count and
query latency, and that measurement has not been made.

Adaptive defaults cost reproducibility, so resolution is logged at startup
with each value marked auto or pinned. Nothing here overrides an explicitly
set variable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Fix runtime sizing and benchmark convergence

* Make runtime sizing transparent across platforms
* Require authenticated demo access

* Fix adaptive ramp latency baseline
* Add layered runtime configuration

* Harden runtime configuration loading

* Finalize namespaced configuration contract
resolveUser returned as soon as a linked (issuer, subject) was found, so
allowed groups, allowed domains, and group-to-role mappings were only ever
evaluated at first login. Removing a user from an allowed IdP group did not
prevent their next login, and removing them from an admin group did not drop
their local admin role: configuration that reads as ongoing policy behaved as
first-login provisioning policy.

Linked logins now re-evaluate the configured allow policy against the claims
presented now, including the email used for domain matching. When a group-to-
role mapping is configured the IdP owns the fixed role; the role change, auth
version increment, session revocation, and audit event are one transaction via
UpdateWithAudit. With no allow policy or no role mapping configured, locally
managed membership and roles are left alone.

The last active administrator is never demoted, so an installation cannot lose
the ability to administer itself.

Entra ID group overage is handled explicitly: past roughly 200 memberships the
groups claim is omitted and _claim_names names a Graph API source instead.
Reading that absence as an empty group set would demote or lock out an entire
directory at once, so a token signalling overage is denied with an error that
names the remediation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first-administrator credential carried 48 bits and was cleared only at the
end of a successful setup request, after fallible ingest work. The handler also
established a session for an existing admin on ErrSetupComplete, so one
credential could mint several sessions.

- Generate 128 bits and deliver them inside the setup URL rather than as a
  dash-grouped code. PR #82 reduced entropy for typing convenience, but the
  credential is pasted alongside a URL, so length costs nothing in usability.
  The login screen reads setup_token from the query string and strips it from
  the address bar so it does not linger in history or a shared screen.
- Consume the credential immediately after the first administrator commits,
  before any other work. A later failure in that request no longer leaves a
  live administrator credential behind.
- Deny ErrSetupComplete with 403 and establish no session.
- Warn in the startup banner that the URL is a credential and that stderr may
  be collected by container logs and log shippers.

Denying the retry path removes the only route back in for an installation whose
session establishment fails after commit, so docs/authentication.md documents
the recovery procedure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Verify read the attempt count, compared it against the limit, and incremented
in a separate statement. Concurrent requests could all observe a count below
the limit and evaluate more guesses than the three the code advertises; a
32-way concurrent test evaluated 18.

Each guess now reserves its attempt with a conditional update and checks the
affected row count before the code is compared. Single-use behaviour for a
correct code is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
POST /oauth/register validated its input but had no keyed limiter, so an
attacker could create an arbitrary burst of clients before the janitor
collected the abandoned ones. Registration is now bounded per source.

Registration is also unauthenticated by design, which means client_name on the
consent screen is attacker-chosen: a client registered under a trusted-sounding
name is indistinguishable from a legitimate one. The consent card now marks
dynamically registered clients as unverified and emphasises the redirect host,
which the name cannot forge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers the first-run setup URL and its log-exposure risk, the login paths that
must exist before setup completes, and a three-step recovery procedure for a
locked-out installation.

Documents the trusted-proxy configuration that rate limiting and audit
attribution depend on: forwarded client addresses are ignored by default, and
FANOUT_TRUSTED_PROXY_CIDRS becomes the complete trust set when configured.

Documents the OIDC behaviour that changed in this branch: eligibility and role
mapping are reconciled on every login, the last active administrator is never
demoted, Entra ID group overage is denied with its remediation, and email
verification defaults to required.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review of the preceding four commits found the reconciliation work applied
unevenly and the token-shape handling too narrow.

- Enforce the allow policy on the linking login, not only on later ones. An
  administrator-created local account could link an identity that the policy
  rejects and then be denied on every subsequent login. This replaces the
  issuer-mode-only check, so one code path now decides eligibility.
- Report an unusable email claim as such. Normalizing a missing or malformed
  claim to an empty string turned a provider misconfiguration into "identity
  no longer satisfies the allow policy" for every user at once, naming the
  wrong cause. The group allow list is still evaluated first, so an unusable
  email only matters when a domain policy has to decide.
- Detect the hasgroups form of Entra group overage in addition to _claim_names,
  and decode both as raw JSON. A provider emitting an unexpected shape for
  either previously failed the whole token unmarshal and denied every login.
- Skip role reconciliation for a deactivated account. The caller denies it, but
  reconciliation had already rewritten the stored role, revoked sessions, and
  written a role.changed event on every rejected attempt.
- Read the setup token from the URL purely and strip it in an effect. Doing
  both in the useState initializer meant a re-invoked initializer under
  StrictMode read the already-stripped URL and dropped the credential.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A local smoke test showed configuration validation already refuses to start
without SMTP in local mode, or without an issuer and client in OIDC mode, so
the documented scenario of an installation with neither was wrong. The real
risk is a login path that is configured but not working, which is what the
recovery procedure addresses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…isioning

Second review of the branch found reconciliation still applied unevenly.

- Reconcile the role on the linking login, not only on later ones. A local
  account promoted to admin through the UI received a full admin session on its
  first OIDC login and was demoted only on the second, contradicting the
  documented "on every login the mapped role is applied".
- Evaluate eligibility before anything is created or linked. Under Entra group
  overage an auto-provisioned login was correctly denied, but only after the
  user row and a successful user.provisioned audit event had been committed.
- Record the source IP and user agent on role.changed. It is the most
  security-relevant event this handler writes and was the one event that could
  not be correlated during an incident review.
- Ask CountActiveAdmins before attempting a demotion that the last-active-admin
  invariant will refuse. Discovering it inside UpdateWithAudit opened a write
  transaction and logged a warning on every login for a condition only an
  operator can change.
- Widen the registration budget to 60 per 15 minutes. The limiter keys on
  client IP, which collapses to a single bucket behind a proxy when
  server.trusted_proxy_cidrs is unset, so a team rollout of MCP clients could
  exhaust the previous 20.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The setup-token change in ui/host/src/auth.tsx alters the built bundle, and
internal/ui/dist is committed and verified by just ui-check. Content hashes
change across the shared chunk graph, so the diff is wider than the one source
file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Apache-2.0 section 6 withholds trademark rights but says nothing about what use
is welcome, so contributors and downstream users have no line to work from.
This records it: truthful references, unmodified redistribution, and writing
about Fanout need no permission; branding a modified build or a hosted service
as Fanout does.

The policy is the practical protection a permissive licence leaves available —
it is what makes "Hosted Fanout by someone else" actionable while keeping every
right Apache-2.0 grants over the code intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Allow telemetry evaluation without AI or SMTP. Login links use purpose-bound 128-bit credentials stored as HMAC hashes and expire after 15 minutes.

Closes #168
Keep first-admin credential expiry mutation out of the production API.

Closes #169
BREAKING CHANGE: remove ingest.public_endpoint and
FANOUT_INGEST_ENDPOINT. Use ingest.advertised_endpoint and
FANOUT_INGEST_ADVERTISED_ENDPOINT.

The setting only controls collector guidance; it does not expose the
OTLP listener.

Refs #170
Finish the pre-release configuration cleanup before compatibility aliases become necessary.

BREAKING CHANGE: YAML keys and interval environment variables now use ai.* and unit-bearing duration names. Removed names are rejected.

Closes #170
Add authenticated OTLP/HTTP, no-SMTP user onboarding, release artifacts, operator guidance, immutable workflow pins, and remove OpenSpec.

Closes #172
Refs #95, #166, #168
Pin patched transitive versions until the MCP SDK resolves them
directly. Add bun audit to the shared local and CI gate with no
exceptions.

Refs #166
Harden the public release path, add reproducible legal notices, withdraw the unverifiable benchmark claim, and define community support boundaries.\n\nRefs #166
Avoid tar SIGPIPE under pipefail when checking release legal files.\n\nRefs #166
vishr and others added 17 commits August 22, 2026 17:51
Require Authorization: Bearer for OTLP/gRPC and OTLP/HTTP. Rename the browser mutation and delete confirmation headers.

BREAKING CHANGE: x-fanout-ingest-token, X-Fanout-Request, and X-Fanout-Confirm-Delete are no longer accepted.
Auto-provision verified OIDC identities as viewers without shared credentials. Keep chat, MCP tools, and owned dashboards available while operator mutations remain protected.

Derive the MCP OAuth resource from the canonical public URL, bind transport requests to its Host, and enforce email verification on every login.

BREAKING CHANGE: mcp.public_url and FANOUT_MCP_PUBLIC_URL are removed. Set FANOUT_PUBLIC_URL instead.
Public installations need per-user viewer access without a shared credential or external identity provider. Provision only after email-code verification and never before admin setup.
An empty namespace is the product-wide default, but SQL equality treated it as a literal and made default dashboards appear empty.
Replaces the single hand-written documentation page with 33 pages on Starlight
in a Diátaxis tree, twelve of them generated from the code they describe and
gate-checked against it by `just check`.

The site serving fanout.run was one 419-line page whose source was on no live
branch — the 2026-08-21 history rewrite dropped `site/` and it was never
re-added, so a site edit had no path to production.

Generated by `cmd/fanout-docgen`: nine settings pages from `config.Config`
struct tags (52 settings), the alert expression variables from `alert.AlertEnv`
(11), the role/capability matrix from the middleware's own table, and the
application HTTP routes with the credential each requires (45).

The route and role pages ask the running authorization logic rather than
re-reading it. `internal/api` gains `DescribeRoute`, `RoleCapabilities` and
`AllCapabilities` — additive, read-only accessors that expose what
`classifyRoute` and `roleCapabilities` already decide. A generator that parsed
those instead would be a second authorization model free to drift from the one
that runs.

That yields guards worth more than the pages: a route registered without a
matching case in `classifyRoute` fails the build, as does a route whose group
prefix the generator cannot resolve, a configuration group with no authored
description, and a capability with no explanation. Each was verified by
breaking it deliberately.

Pages were written against the source rather than ported. The archived page
claimed native OTLP/HTTP was unimplemented, that an AI key was required, that
SMTP was required for local sign-in, and documented a `PUBLIC_INGEST` bypass
that no longer exists — none carried forward. A review caught two more of the
same kind that had crept into the new pages: five telemetry endpoints published
as requiring no credential, and a claim that `viewer` cannot run the chat
investigator when the middleware grants it. Both are fixed, and both are now
generated so they cannot recur.

For agents: `llms.txt`, `llms-full.txt`, and a Markdown alternate at
`<path>.md` for every page, advertised in the head. The index carries each
page's own summary and `read_when` lines and annotates anything not shipped, so
a marker arrives before the page does. Every page states its status explicitly
rather than relying on the `shipped` default — 29 are `preview`, on evidence:
in the week this was written the ingest header was replaced,
`FANOUT_MCP_PUBLIC_URL` was removed, namespace scoping inverted, and `viewer`
gained `agent:run`.

Also here: the README's Scope section names `site/` as a stated exception with
its reason; `.github/workflows/site.yml` publishes from main to Cloudflare
Pages behind the same generator check; and CI installs the site's dependencies
so the gate can build it.

Not yet publishing — a `fanout` Pages project and `CLOUDFLARE_API_TOKEN` do not
exist yet, and fanout.run keeps serving from its existing container until they
do.

Follow-ups: #181 (MCP tool generation), #188 (routes registered outside
`internal/api`), #189 (a pre-existing flake this surfaced).
…ered (#191)

The route reference was built by scanning `internal/api` alone, so it omitted
sixteen routes on a page whose own `read_when` invites an operator to audit
what an instance exposes. Someone using it to enumerate exposure concluded
`/debug/pprof/*` and `/-/metrics` were not served.

Missing were the eleven operational, protocol and SPA routes wired up in
`cmd/fanout` — `/-/metrics`, six `/debug/pprof/*`, `/mcp`, `/api/mcp`, and the
SPA catch-all — and the five `/api/agent` routes in the agent runtime, which
the caveat did not even mention because it named only the first group.

`POST /api/agent` was missing for a second reason worth stating separately: a
group's own root is registered as `group.POST("", ...)`, and the collector
required a leading slash, so the one route that actually runs the investigator
was dropped silently rather than refused. An empty relative path is now a real
route when the receiver is a known group.

`Any` registrations are collapsed to one row only when the middleware gives
every method the same answer; where it would not, the build fails rather than
publish a requirement that is wrong for some verb. Both current `Any` routes
qualify.

The scan list is a flag with each directory named. A directory in it that
registers nothing is an error, so the list going stale is loud rather than a
quietly shorter table — the same property the existing per-run check had, moved
to per-directory. It counts what a directory registers rather than how much it
adds to the total, because a route registered in two places would dedupe to
nothing new and read as a directory registering none.

The page now says what it covers instead of carrying a caveat, and states which
groups are conditional: pprof off by default, MCP on by default, the agent only
with a provider key. That distinction is what an exposure audit needs, and the
previous text gave none of it.

Each guard was verified by breaking it: an unregistered group prefix, a scan
directory that registers nothing, and the per-directory count — the last fails
the build over a duplicated directory if it counts newness instead. Routes went
45 to 61.

Closes #188.
* docs: add a social preview card and a comparison table

Two gaps in how the repository meets someone who has just found it.

Links to this repository unfurled as a generic grey box on GitHub and in
every chat client. `docs/media/social-card.png` is the image to upload as
the repository's social preview; it is generated from the `.typ` beside it
rather than hand-drawn, so the next edit is a text edit. The header comment
carries the render command, including the font path the card needs — Inter
is not installed system-wide, and without it the card silently falls back
to a substitute face.

The README never contained the words Grafana, Loki, Tempo, SigNoz, Jaeger,
Prometheus, Datadog, Honeycomb, or ClickHouse, which is what someone types
when they are shopping in this space. Each row states the boundary
difference rather than a feature score, and the section closes by saying
outright that a deployment which outgrows one machine has outgrown Fanout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: gate the social card instead of trusting it

Review of the first commit found three ways the card could go wrong quietly.

The mark was a fourth hand-copy of the ribbons, sitting outside the guard
that internal/brand keeps against ui/host/public/favicon.svg. A ribbon
revision would have updated MarkSVG and its testdata under test pressure
while this copy kept the old geometry indefinitely. Typst reads the
canonical favicon directly now; the render is unchanged.

The PNG had no recipe. `just social-card` renders it and does the part a
hand-typed command cannot: typst warns about an unknown font family and
still exits 0, so the recipe reads the warning back and fails, and renders
to a temporary file so a fallback-face card is never left in the tree. The
typst version is pinned for the same reason d2 0.7.1 is.

The footer was placed out of flow at a fixed offset, so a longer headline
would have drawn the divider straight through the body copy with typst
reporting nothing. It is pushed down by flexible space instead: the same
overflow now spills onto a second page, and a two-page render fails the PNG
export outright.

Verified by rendering: the card from the canonical favicon is identical to
the committed one, the recipe fails against an empty font directory and
leaves the committed PNG untouched, and a deliberately overlong headline
fails the export rather than overlapping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: drop a pricing claim about other vendors

The managed-services row said those products carry per-gigabyte pricing.
Their pricing models differ from one another and change without notice, so
the claim was both a swipe and a fact this repository cannot keep true. The
row now says what is actually different: someone else runs the system, and
the telemetry leaves the network to reach it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: correct the card's own instructions

The header still gave a bare `typst compile` line, which no longer works:
the mark now comes from ui/host/public/favicon.svg, and typst sandboxes an
input to its own directory, so the documented command fails on the image
unless `--root .` is passed. It points at `just social-card` instead, which
passes the root, pins the version, and fails on a missing font — none of
which a hand-typed command does.

`mktemp -t social-card` also reserved a name and the recipe appended `.png`
to it, so typst wrote to a path nothing had reserved and the reserved file
was left behind on every run. A temporary directory reserves the whole name.

Verified: `just social-card` re-renders the committed PNG byte-identically.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: give the new site the card as its og:image

Rebasing onto main brought the documentation site with it, and the site has
the same gap the card was made for: Starlight emits
`twitter:card: summary_large_image` and never an image to pair with it, so
links to fanout.run unfurled as an empty large card.

The card moves from docs/media to site/public, where GitHub still takes it as
the repository preview and the site can serve it at /social-card.png. The
recipe writes there now, and `site-build` runs a check beside check-tables
that asserts every built page carries an og:image, that the target is in the
build, and that its real PNG dimensions match the hand-typed width and
height — CI has no font path to re-render the card with, so that is the part
a gate can honestly do.

Ran `npm run build` in site: 33 pages, both checks pass, and the built head
carries og:image pointing at https://fanout.run/social-card.png.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#193)

* docs: generate the MCP tool reference from the server's own tools/list

The MCP reference was hand-written, and it was the last page describing a
surface a reader acts on with nothing keeping it level with the code. It named
four input names. The server exposes eleven.

Absent entirely were `trace_id` — the input that makes `trace_detail` fetch a
specific trace rather than a guess — `search` and `severity` on `search_logs`,
and every input of all four dashboard tools, including which are required. The
page also stated that `service_performance` and `trace_detail` "additionally
accept `service`", which reads as a closed list and is wrong: `search_logs`
accepts it too.

`internal/mcp/describe.go` follows internal/api/describe.go. It does not read
the registration calls: it starts a real server over an in-memory transport and
issues `tools/list`, the same request a connecting agent makes, and publishes
the answer. A generator that parsed registerTools would describe the shape of
the code; this asks the server the question its clients ask, so a tool renamed,
re-annotated, or given a new input cannot be documented as it used to be.

Mutation semantics come from the server's annotations rather than from the
tool's name. Absent hints default to true per the MCP spec, because reading a
missing destructiveHint as false would publish a mutating tool as safe — and
"this tool does not modify anything" is a claim someone connects an agent on.

The server is built with a non-nil dashboard service deliberately: a nil one
registers no dashboard tools at all, so the page would have quietly described
five tools instead of nine, two of which mutate. A test pins that.

site.yml listed only internal/config as a generator input while claiming to
enumerate them. It now lists all of them — internal/alert, internal/api,
internal/agent, internal/mcp and cmd/fanout had accumulated behind it.

Verified locally: the drift guard fires (a changed tool description fails
`--check` naming the page, and passes again on restore); the summary table's
anchors resolve in the built HTML; the three internal links exist; llms.txt
indexes the page with its summary and both read_when lines; `-race` and
`-count=3` are clean.

Closes #181.

* docs: check the MCP page's claims instead of asserting them

Review findings on the generated MCP reference.

The page stated "Every tool is closed-world: it reads or writes what this
instance holds and reaches nothing else" as flat prose, while ToolDoc.OpenWorld
was computed and never read. A tool registered without an openWorldHint defaults
to open-world per the spec, and would have been republished under that blanket
promise. `--check` could not catch it: a regenerated page carrying the same
false claim is self-consistent. The renderer now refuses.

Tool titles and descriptions were interpolated into MDX with no escaping. A
description containing a pipe would have split its table row into phantom
columns — wrong output rather than broken output, so nothing downstream would
have noticed; check-tables.mjs asserts that tables are wrapped, not that their
rows have the right shape. One containing `<` or `{` would have failed the site
build instead. Both are escaped now, along with newlines, which end a row.

The escaping is a no-op on every current description, which is why it needed
adversarial input to verify rather than a passing build.

Also: an empty Title was accepted where an empty Description was refused, and
the renderer emits the title as `**%s** — description`, so it would have
published a heading beginning with stray emphasis. The scope test filtered by
tool name and asserted nothing when the filter matched nothing, so renaming
either tool would have switched it off silently; it also compared inputs as a
subset, so a field added to QueryInput would have passed while the page's claim
that the two share one scope went stale. And the ServerSession was leaked on the
client.Connect error path — the same leak the deferred close exists to prevent,
on the path that returns before reaching it. The handshake is now bounded, so a
transport that never completes fails the build rather than hanging it.

Each guard verified by breaking it: dropping OpenWorldHint, blanking a Title,
and putting `p50 | p95 | p99 over a <window> with {braces}` in a description —
which now renders as one cell of literal text and builds.

* docs: fail on a schema or tool name the MCP page cannot render

Second review round. Three guards; the generated page is byte-identical.

`schemaType` returned `any` for a property whose schema names no type, while
`toolInputs`' own comment two functions above says it "refuses anything it does
not recognise rather than treating an unreadable schema as 'no inputs'". The
comment described what was meant, not what the code did. The SDK emits `$ref`,
`anyOf` or a bare `enum` for shapes it cannot reduce to one type — making an
input a pointer or an interface is enough — and `any` publishes as a deliberate
"accepts anything" rather than "not determined". It is an error now, naming the
keys the schema does carry so the message points at the cause.

The summary table links each row to the tool's own section, whose heading is the
tool name. That holds only while the name is already what a slugger would
produce from it: a name with a capital or a dot slugs to something else and
every row link on the page goes dead. Nothing downstream catches a dead in-page
anchor — `--check` compares the generator's output to itself, check-tables.mjs
checks wrappers, and the build succeeds — so the assumption is now checked where
it is made.

site.yml's check step still described the reference as generated from
internal/config, internal/alert and internal/api, contradicting the trigger
paths eight lines above that now correctly name internal/agent, internal/mcp and
cmd/fanout too. It points at that list rather than restating it, so the next
source to be added cannot leave it stale again.

Verified by breaking each: a tool renamed to `Observability.Overview` is
refused by name, and schemaType has tests for `$ref`, `anyOf`, a bare enum,
`["null"]` and an empty type, plus that it still renders unions with null
dropped.
* feat(ui): put the web app on the documentation site's palette

The app ran on Mantine's stock theme: teal primary, Inter named but never
loaded, and no dark mode at all. The documentation site, meanwhile, has a
palette with reasoning behind it — Ayu Dark surfaces, a violet accent, IBM
Plex split by role — and a comment claiming the app already shared it. It
did not. Nothing crossed between them: not a hex, not a typeface, not a
variable name.

The teal was the substantive problem. `healthColor()` returned "teal" for a
healthy service while `primaryColor: "teal"` painted every button and link,
so one hue meant both "healthy" and "clickable" on the same screen. The site
picked violet precisely to avoid that, violet being the one hue in the
palette that carries no status.

ui/tokens.ts now holds the palette — the Ayu ramp, the violet accent, the
three health hues and a categorical series palette taken from the brand
mark — and ui/theme.ts binds it to Mantine under semantic names: brand, ok,
warn, bad, info. A component asks for the accent or for a health state and
gets whatever the palette holds, so re-hueing the product is a change to one
file. The values mirror site/src/styles/fanout.css; the two are still
separate sources kept in step by hand.

Dark mode ships with it: defaultColorScheme="auto", a header toggle, and the
scheme resolved by an inline script in the document head so the first paint
lands on the right ground. The typeface is shipped rather than named — eight
weights in the host, four inlined into each single-file embedded view, which
needed assetsInlineLimit raised or the build would have emitted loose .woff2
files it never copies.

Three latent bugs surfaced, all of them invisible while the app was
light-only: the sign-in card hardcoded a white background, dashboard metric
tiles used bg="gray.0" and rendered white-on-white, and the MCP app bridge
passed theme: "light" as a literal, which would have left every embedded
view light inside a dark host.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(docker): copy every shared module into the browser build stages

The two browser stages copied `ui/theme.ts` by name, so `ui/tokens.ts` never
reached the container and both `bun run build` invocations failed on an
unresolved import. Globbing the directory means the next shared module does
not have to remember this file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#195)

Patch and minor drift only, across both browser workspaces: Mantine 9.5.2,
TanStack Query 5.102.2 and Router 1.170.32, AG-UI client 0.0.58, uuid
14.0.2, Vite 8.2.2, @vitejs/plugin-react 6.1.0, happy-dom 20.11.6 and the
React DOM types. The security overrides in each package.json are untouched,
so installs, local checks and CI keep resolving the same pinned fixes.

The embedded assets and the notices file are regenerated in the same commit,
since both are committed outputs that their own checks compare against a
fresh build.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
#196)

`go get -u ./...` plus a tidy: five direct dependencies and the transitive
graph behind them. cel-go 0.31.0, modernc.org/sqlite 1.57.0, grpc 1.83.1,
protobuf 1.36.12 and the ag-ui community SDK. No major versions and no API
changes — the diff is go.mod, go.sum and the regenerated notices.

The Go toolchain stays pinned at 1.26.5. Moving it is a separate decision
with CGO and DuckDB implications, and 1.27 changes both encoding/json's
escaping of invalid UTF-8 (which internal/ingest asserts against) and the
module graph the notices file is generated from.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The workflow actions are pinned to commit SHAs rather than tags, which is
what makes this worth configuring: a pinned SHA never moves on its own, so
without something proposing new ones the pins quietly rot, and resolving
them by hand is exactly the motion the pinning exists to prevent. Dependabot
resolves each new SHA, names the release it belongs to, and opens one
reviewable pull request.

Five graphs are covered: the actions, the Go modules, the two browser
workspaces (separate on purpose, each with its own overrides block pinning
security fixes) and the npm-based documentation site. Minor and patch
updates are grouped per ecosystem so a week of drift arrives as one pull
request; majors are left ungrouped so they arrive alone, since those are the
ones that need someone to read the migration notes.

Note that Bun is a version-updates-only ecosystem for Dependabot — it does
not receive security updates, so `bun audit` in `just check` stays the gate
for those.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 to ed142fd0673e97e23eac54620cfb913e5ce36c25.
- [Release notes](https://github.com/aquasecurity/trivy-action/releases)
- [Commits](aquasecurity/trivy-action@a9c7b0f...ed142fd)

---
updated-dependencies:
- dependency-name: aquasecurity/trivy-action
  dependency-version: ed142fd0673e97e23eac54620cfb913e5ce36c25
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file github_actions Pull requests that update GitHub Actions code labels Aug 24, 2026
@vishr vishr closed this Aug 24, 2026
@dependabot @github

dependabot Bot commented on behalf of github Aug 24, 2026

Copy link
Copy Markdown
Author

OK, I won't notify you again about this release, but will get in touch when a new version is available. You can also ignore all major, minor, or patch releases for a dependency by adding an ignore condition with the desired update_types to your config file.

If you change your mind, just re-open this PR and I'll resolve any conflicts on it.

@dependabot
dependabot Bot deleted the dependabot/github_actions/aquasecurity/trivy-action-ed142fd0673e97e23eac54620cfb913e5ce36c25 branch August 24, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant