Repository navigation
Releases: librefang/librefang
Release list
v2026.9.19
Full Changelog
56 PRs from 3 contributors since v2026.9.14.
Highlights
- Pause and resume autonomous goal runs — a long-horizon run can now be suspended and picked up later from the API (
POST /api/goals/{id}/pause/resume), the Goals page, or the TUI's Goals screen withp, and a resumed run continues from its checkpointed iteration with its retry budget and captured lessons intact. - Per-turn model routing and spawn profiles — an agent can pick a model per turn by task complexity instead of being pinned to one in its manifest,
agent_spawngains aprofileparameter that pins a spawned agent onto a named profile, and complexity keywords now match whole words so a task is no longer misrouted on a substring. - Goal runs got a lot more honest — a refused start is reported as a failure instead of success, a throttled verifier is no longer read as a missing one, work the verifier just rejected can no longer close out the goal, and a second run stops deleting the first run's captured
GOAL_LEARNED:lessons. - Workflow run timeline in the dashboard — the Workflows page surfaces each run's live step progress and outcome as it executes, and the chat transcript gains a size control that spends less of the window on chrome.
- Registry promotion is configurable and safer — a new
[skills.promotion]section replaces values that were hardcoded on the GitHub side,api_base_urlno longer accepts a plainhttp://origin outside loopback, and proposing to the registry refuses to push to a same-named repository under the fork owner without an explicit opt-in.
Building from source now requires Rust 1.95.0 (up from 1.94.1).
Added
agent_spawngains aprofileparameter that pins the spawned agent onto a named model profile.
This is the case model profiles were built for: the goal loop auto-spawns helpers, and without it a verifier whose only job is to answer "is this done?" inherits its parent's expensive model.
Passingprofile = "quick"now writes that profile's provider and model onto the child's manifest instead of letting it inherit the default.
Naming a profile that does not exist fails with the list of profiles that do, rather than spawning an agent on the wrong model.
The parameter is honoured whether or not[model_router] enabledis set: that switch governs the automatic per-turn router, while naming a profile here is an explicit choice, so a cheap sub-agent does not require turning on routing for every agent (#7757) (@DaBlitzStein)- Add complexity-based model routing so an agent can pick a model per turn instead of being pinned to one in its manifest.
The common deployment reality is a cheap model that handles most turns and an expensive one that should only see the hard ones; until now that choice was static per agent, so operators either overpaid on every trivial turn or underserved the hard ones.
AModelProfilebinds task tags to a provider/model pair, a cost tier and a complexity ceiling; the kernel scores each turn from its text and picks the best profile the agent is permitted to use.
Builtin profiles ship as an asset and are overridden per-name from~/.librefang/model_profiles.toml, which is re-read when its mtime moves so an edit needs no restart.
Off by default: set[model_router] enabled = trueinconfig.tomlandmode = "flexible"in an agent's[model]block.
Per-agent constraints live in[model.router_override]—allowed_profileslimits the choice,cost_budgetcaps the tier,default_profileis the fallback, andfixed = trueopts the agent out entirely.
A fallback profile is re-checked against those same constraints, so adefault_profilecan never spend past an agent's cost budget.
Configurable from all four surfaces:GET/PUT /api/agents/{id}/model_routingandGET /api/model-router/profileson the API, a Routing tab on the dashboard's agent detail, anreditor on the TUI agent detail screen, andlibrefang agent routing/routing-set/routing-profileson the CLI (#7757) (@DaBlitzStein) - Loop engineering for autonomous goal runs, opt-in per goal.
A run ended the moment the agent wroteGOAL_DONE, which left the worker as the sole judge of its own work — the one check a long-horizon loop most needs, and the one it did not have.
Settingloop_engineeringon a goal adds two judges that are not the worker.
A verifier agent (verify_agent_id) reads each iteration's output and returnsVERDICT: PASS|FAIL|NEEDS_REWORK; a rejection goes back to the generator carrying the verifier's stated reason, up toverify_max_retriesrework rounds, and until the verifier passes the workGOAL_DONEdoes not end the run.
An evaluator model (evaluator_model) makes one cheap yes/no read of the goal against the latest output, and can conclude the goal is met even when the agent never claimed it.
Because the verifier now stands between the agent and the end of the run,GOAL_DONEchanges meaning: it is a request to finish, granted once the verifier passes the work it is attached to, rather than the finish itself.
The agent can also record a reusable lesson withGOAL_LEARNED: <one sentence>; captured lessons are replayed into later iterations' prompts, persisted to the shared store, and filed as a draft skill in the workshop'spending/queue.
Turning a run's lessons into a skill an agent actually loads still takes a humanlibrefang skill pending approve— an autonomous loop proposes, it does not grant itself authorship.
All three are inert unless the goal opts in, so a goal that does not ask for them sends the same prompt and makes the same single LLM call per iteration it always did.
Sub-agents are delegated rather than provisioned: the prompt directs the agent to its ownagent_spawn/agent_sendtools, which run under the capabilities its operator granted it, and neither the runner nor the API ever creates an agent on a caller's behalf.
First of the five PRs splitting the closed #6505 (#7785) (@DaBlitzStein) - Suspend a long-horizon goal run and pick it up later with
POST /api/goals/{id}/pauseandPOST /api/goals/{id}/resume.
Until nowstopwas the only way to halt a run and it discarded the run's state, so an operator who wanted to free an agent for an hour could only express that as "throw away the last forty iterations and start over".
A pause lets the loop finish the turn it is on, then checkpoints its iteration count, progress and start time, so the run stays visible inGET /api/goals/{id}/runin the newpausedphase while nothing is being spent on it, and a resume continues the same run under the same iteration budget rather than beginning a new one (#7973) (@DaBlitzStein) - Set how often a goal run prompts its agent with
tick_interval_secson the goal itself, accepted byPOST /api/goalsandPUT /api/goals/{id}.
Every goal previously ticked at the same hard-wired cadence, which is the wrong rate at both ends: a goal whose progress depends on something outside the daemon burned turns re-reading an unchanged world, and one that needs to react quickly could not be told to.
An out-of-range value is refused rather than clamped, so an operator who types a cadence learns their number was rejected instead of discovering later that the loop runs at a rate they never chose (#7973) (@DaBlitzStein) - Add a workflow run timeline to the dashboard's Workflows page, surfacing each run's live step progress and outcome as it executes.
The page now polls a selected run every 3 seconds while it is active — including while paused at an operator gate — and stops once it reaches a terminal state, so an operator watching a run does not need to reload to see it advance past an approval or finish. (#7997) (@DaBlitzStein) - The Goals page gains pause and resume controls for the autonomous goal run: a pause button next to stop while the run is active, and a resume button (with stop) while the run sits at its checkpoint.
The controls call the newPOST /api/goals/{id}/pauseand/api/goals/{id}/resumeendpoints, pause stays a run-level phase rather than a goal status, and the badge and progress display keep deriving from the run phase (#8029) (@DaBlitzStein) - A
[skills.promotion]config section for the registry promotion flow, which previously had almost every GitHub-side value hardcoded or derived at runtime.
api_base_urlmakes "Propose to Registry" usable on GitHub Enterprise at all, where a compiled-inapi.github.comleft it with no workaround.
commit_author_name/commit_author_emailfix the attribution of the pushed commits, which GitHub otherwise credits to whoever owns the token — wrong for a shared or service token, and not something an operator could correct.
fork_owner,base_branch,head_branch_prefixand amodeofforkordirect_pushcover the organisation-owned fork, the non-default target branch, the branch-naming convention and the internal registry nobody is meant to fork.
Every field is optional and reproduces the previous behaviour when unset, so an installation that configures nothing sees no change. (#8179, #8163) (@DaBlitzStein) - The TUI's Goals screen can now pause and resume a run with
p, which it could already show but not act on.
mainalready colours a paused run yellow and shipstui-goals-phase-pausedandtui-goals-run-paused, so before this an operator watching from a terminal saw a run somebody had paused from the dashboard, correctly labelled, with no key that touched it.
The key reads the live run phase rather than the goal document: a running goal pauses, a paused one resumes, and one doing neither is left alone rather than being started, becausesis the key that starts a run and a pause key that quietly launched one would be a surprise on a screen where "stopped" and "paused" sit next to each other.
Resume sends no body, which is the daemon's "keep the cap the paused run was already under...
v2026.9.14
Full Changelog
203 PRs from 5 contributors since v2026.8.30.
Highlights
-
Goals system — a new
goalcommand and Goals tab in the TUI let you define and track autonomous goal runs, with live phase badges, iteration counts, and inline error reporting. -
Agent token footprint & analytics — dashboards now surface each agent's injected token usage and recent call history, alongside a date-range picker with cost and SLO metrics.
-
Channel multi-instance support — a single channel adapter (e.g. Telegram) can now run as multiple named instances, each with its own configuration and secret namespace, manageable from both the TUI and dashboard.
-
Per-agent reasoning mode and aux LLM chain — DeepSeek V4 and OpenRouter agents can be given a per-task reasoning mode; a new UI lets you configure an auxiliary LLM chain for side tasks separately from the primary model.
-
Schema-driven config editor and vault write API — the Settings screen now provides a schema-driven configuration editor, and a new vault write API lets surfaces store daemon credentials securely without manual file edits.
-
Agents spawned from a template now carry the template name forward as
source_templateon the manifest, and the dashboard agent list shows it next to the schedule so operators can trace provenance (#8018) (@DaBlitzStein)
Added
- Autonomous goal runner with an opt-in "loop engineering" mode (#6505): a goal can now drive itself across iterations without a pre-assigned agent —
POST /api/goals/{id}/startauto-spawns a disposable worker agent when none is set, and, when the goal'sloop_engineeringflag is on, auto-spawns a verifier agent that judges each iteration's output before progress is accepted, auto-spawning a fresh reviewer after two consecutive rejections for a second opinion.
A separately configurableevaluator_modelcan judge goal completion instead of trusting the agent's self-reportedGOAL_DONEmarker alone, andGOAL_LEARNED: <text>markers captured during a run are accumulated, injected back into later iterations' prompts, persisted to shared memory, and turned into an auto-created skill on run completion.
Exposed via a newlibrefang goal "<description>" [--loop-engineering] [--watch]CLI command, a TUI Goals screen (list / create / edit / start / stop / delete, with a guided create form), a dashboard agent selector on the Goals page, and a/goalslash command in the Telegram and Slack sidecars (#6505) (@DaBlitzStein) - Add
agent_spawnephemeral mode for Claude Code-style disposable sub-agents — spawn a temporary worker that runs a single task and returns the result directly, with no workspace, no DB persistence, and no registry entry.
A worker's advertised tools are derived from the parent's own set, so a restricted parent cannot launder a privilege escalation through one.
POST /api/agents/spawn-ephemeralis the HTTP entry point, and the dashboard's Quick Run drives it (#7875, #7903) (@DaBlitzStein) - Add write operations (POST/PUT/DELETE) to the
/api/templatessurface, so an operator can create, update and delete named agent types from the dashboard instead of hand-editing TOML on the daemon host.
The listing is deliberately dual-source: an operator-authored agent type under~/.librefang/agent-types/is a standalone document that this API owns, while a live agent's ownagent.tomlhas always been spawnable-from and is listed alongside it.
Only the first is writable, so every row carries aneditableflag and a write aimed at the second is refused rather than silently creating a shadowing copy — a client can then render "managed via Agents" instead of offering a control that cannot work.
New dashboard page: Agent Types, a card grid with create / edit / delete plus Quick Run for an ephemeral spawn (#7859) (@DaBlitzStein) - Add the
workflow_createtool so agents can create reusable workflows directly during a conversation — previously workflows could only be created via the dashboard canvas, CLI, or HTTP API.
The tool validates names ([A-Za-z0-9_-], 1–64 chars) at both the runtime and kernel layers, runs the sameWorkflow::validate()semantic checks as the HTTP API, reserves the name atomically so two concurrent calls cannot both believe they created it, and ships in the default tool catalogue so an ordinary agent has it without configuration.
Add theworkflow-creatorskill — a prompt-only skill teaching agents when and how to design multi-step workflows, with step structure, execution modes, error handling, and worked examples (#7857, #7873) (@DaBlitzStein) - Workflow steps can declare
required_skills; a step whose resolved agent (or spawned agent type) cannot provide one fails before dispatch with a precise error distinguishing "not declared" from "declared but not installed" (via the pending-declarations plumbing).
All resolver surfaces (kernel sync/async, API background/resume/operator, channel bridge) carry the check, so a step cannot slip past it by being started from a different entry point (#7863) (@DaBlitzStein) - Ephemeral workers spawned from an agent type get a uid display name and a transient mission workspace — created at spawn, deleted when the run ends, success or failure.
The folder is passed as the agent workspace root, so a worker has somewhere to drop intermediates that is guaranteed to be cleaned up rather than accumulating under the operator's home (#7860) (@DaBlitzStein) - The agent-types editor now mirrors the agent editor: catalog-backed pickers for skills and tools (fuzzy search over installed skills / the tool catalog, free-text allowed), provider/model defaults with hints, and workspace-agent source entries are read-only ("managed via Agents").
The TUI templates screen lists operator-created types and spawns them from their real TOML rather than a hardcoded starter set (#7731) (@DaBlitzStein) - Skill cards on the Skills page now link out to the skill's page on the marketplace it came from, so the comments, ratings and full description are one click away — for browse results and for installed skills alike.
The link is only offered where a public page exists: ClawHub and its CN mirror always, a self-hosted SkillHub whenVITE_SKILLHUB_REGISTRY_URLnames its origin, and never for FangHub, which publishes no browsable skill page (#7751) (@DaBlitzStein) - A model's context window and max output tokens are now editable overrides that survive the registry sync which used to overwrite them.
ModelOverridescarried nocontext_windowat all, so an operator pointing LibreFang at an OpenAI-compatible gateway had no way to tell it how large that model's window really is — and no way to correct a catalog entry that reported the wrong one.
Both values now live in the sync-immune overrides side-table, andresolve_context_windowhonours the override even for a model with no catalog entry at all, which is the gateway case.
The API reports the effective value alongside the raw catalog figure so a surface can show which of the two an operator is looking at, andlibrefang models overrides <model>covers the same ground for scripting (#7818, #7990) (@DaBlitzStein) - Drive goals from the terminal, split out of the closed #6505: a
librefang goalcommand and a Goals tab in the TUI, both against the existing/api/goalssurface.
librefang goal "<description>" --agent <name-or-uuid>creates the goal and starts the run,--max-iterationscaps it, and--watchpolls every two seconds and prints the phase, iteration, and progress until the run ends — exiting non-zero when it stops, is rate-limited, or hits the cap, so a shell&&chain does not treat an abandoned goal as success.
The agent is resolved and validated before anything is created, becausePOST /api/goals/{id}/startrefuses a goal with no agent assigned and the previous order left an unrunnable goal behind on every mistyped invocation.
The TUI tab (Alt+G, the first free mnemonic — every numeric slot is already bound) lists goals with a status badge and progress, searches with/, creates with a three-field wizard, and starts, stops, or deletes the selected goal.
Run state — phase, iteration, and cap — is fetched per goal fromGET /api/goals/{id}/runwhen the detail pane opens, since the goal document served by the list endpoint has never carried it; the start/stop toggle keys off that live phase rather than the document'sstatus, which is set toin_progresswhen a run starts and never cleared when it ends (#7784) (@DaBlitzStein) - Add
librefang-types::manifest_privacy, the privacy pass a deployed agent's manifest crosses before it is published to the shared registry as a public agent type.
A live instance'sagent.tomlcarries operator-private data the registry validator does not check for — absolute workspace paths,api_key_env/base_urlcredentials on the model and every fallback, external workspace mounts, host-specificexec_policy, literalcontext_injectiontext, and instance metadata — none of which belongs in a public git history, where it stays.
sanitize_for_publicationreturns a publishable copy with that removed and the portable agent-type half untouched;scan_for_publicationindependently reports what it found, with a bounded preview per finding, including in fields the sanitiser deliberately keeps, likesystem_prompt, where no structural rule separates portable configuration from an internal hostname someone pasted into free text.
Keeping the two halves apart is the point: each finding says whether confirming is enough or the operator has to edit the value by hand, so the operator decides rather than having their file quietly rewritten.
The classification cannot silently rot either — the sanitiser destructuresAgentManifestwith no rest pattern and rebuilds it field by field, so a new manifest field fails to compile until someone decides whether it is publishable (#7819) (@DaBlitzStein) - `r...
v2026.8.30
Full Changelog
554 PRs from 2 contributors since v2026.8.19.
Highlights
- Ephemeral workers — agents can now spawn short-lived worker agents on demand via HTTP or Quick Run, with spend rolled up to the parent
- Workflow canvas — create and edit workflows with named step agent bindings, per-step required skills, reversible agent binding, and contained transient mission workspaces
- Semantic memory for agents — agents can read and write their own memory store directly; session-scoped recall and per-capability access controls are now configurable per agent
- Backups and selective restore — a new Backups tab in settings lets you create, list, restore, and delete backups, with component-level selection on restore
- Identity and access — OIDC role claims can now grant authorization, user groups are a first-class entity, and identity-provider groups map onto local groups for IAM
Added
- Ship the Kubernetes half of managed configuration as a kustomize overlay at
deploy/kubernetes/overlays/managed-config/, so the deployment shape the mode was built for exists as a manifest rather than as prose.
config.tomlis rendered into a ConfigMap and mounted read-only outside the PVC, credentials stay in Secrets and reach the daemon as environment variables, and achecksum/configannotation on the pod template makes a config edit roll the StatefulSet.
The annotation carries sha256 over the file's own bytes rather than kustomize's name-suffix hash, which makes the value that triggers the rollout the same valueGET /api/config/statusreports back — so confirming a change landed is one string comparison instead of an inference from a restart count (#7902) (@houko) - Agents can now spawn an ephemeral worker with
agent_spawn's newephemeral: truemode — one turn that runs a task with a real tool set and then vanishes, leaving no agent record, no persisted session and no workspace, with the answer handed straight back in the tool result.
The point of it is that most delegation is task-shaped rather than colleague-shaped: until now the only way to hand work to a fresh context was to create a permanent agent with seven directories and a database row, and then remember to clean it up.
A worker's tool set is bounded by what the spawning agent may itself call, its spend and resource quota are billed to that agent rather than to nobody, and its nesting is capped by the samemax_agent_call_depthcounteragent_sendandworkflow_runalready share, so a worker that spawns workers cannot recurse without bound.
Every tool the worker is shown is one it can actually execute — it runs against the same kernel, skill, MCP, web, browser and workspace handles a permanent agent's turn does — and a tool name the spawning agent cannot call is refused by name instead of quietly dropped.
Each run gets a scratch mission folder that is deleted when the run ends, on the success path and the failure path alike (#7875) (@houko) - Ephemeral workers can now be launched from outside a running agent turn.
POST /api/agents/spawn-ephemeralis the HTTP entry point to the spawn engine that #7875 shipped, and the Agent Types page gains a Quick Run control that calls it — pick the agent to run on behalf of, type the task, read the answer.
Everything the engine guarantees still holds because the route adds no policy of its own: the advertised tool set is the executable one, the spend and the[resources]quota land on the parent you chose, and the recursion bound is the sharedmax_agent_call_depth.
Resolving an agent type by name now also searches the writableagent-types/store, which is wherePOST /api/templatesand theagent_type_createtool have been writing all along.
Without that, every type the dashboard can create was invisible to the one engine whose job is to run it, and Quick Run would have failed on the entire catalog the page renders.
(#7903) (@houko) - Agents can now define a workflow during a conversation with the new
workflow_createtool, instead of workflow authoring being reachable only from the dashboard canvas, the CLI, or the HTTP API.
A workflow an agent writes is registered immediately and outlives the turn, so the nextworkflow_run— from that agent or any other — can reach it.
Names stay unique: the check and the registration are one atomic operation inside the engine, so two agents proposing the same name concurrently cannot both succeed and leave a workflow that name-based lookup resolves to at random (#7857) (@houko) - Ship a
workflow-creatorskill so an agent reaching forworkflow_createknows the shape of a good workflow rather than guessing at one.
The tool has been available since #7857, but its JSON schema can only describe fields — not when a workflow beats a one-offagent_send, why{"type": "researcher"}binding makes a workflow portable to an instance where nothing is pre-registered, or which of the creation-time validations an author is most likely to trip.
Installing it from the registry puts that in the system prompt.
Two step fields the tool had always accepted are now advertised as well:required_skills, which fails a step before it bills an LLM call, and the per-stepsession_mode(#7873) (#6934) (@houko) channel_memberscan now report everyone a platform lists in a channel, not only the people who have spoken there — the Slack adapter walksconversations.memberswhenSLACK_ENUMERATE_MEMBERSis enabled, and the daemon persists what it finds.
An agent sitting in a shared channel could previously answer "who has talked here?" but not "who is in here?", which is the question people actually ask it.
The two sets are stored apart, with asourcecolumn on each roster row, becausechannel_dmauthorizes a private message against that roster: bulk-filling it would have quietly turned "people this agent has interacted with" into "everyone the workspace lists", letting an agent DM a channel member who has never addressed it.
Enumeration therefore widens what can be reported and never what can be messaged, and speaking is still what earns someone a private reply.
Off by default and behind its own switch, because unlikeSLACK_RESOLVE_DISPLAY_NAMES— which changes how well the daemon names the handful of people who have spoken to it — this changes how many people it stores at all. (#7919) (@houko)- The Slack sidecar can now resolve a sender's real display name and
@handlethroughusers.infoinstead of surfacing the rawU09…id, behind the new opt-inSLACK_RESOLVE_DISPLAY_NAMESknob.
Lookups are cached per user id forSLACK_DISPLAY_NAME_TTL(six hours by default) becauseusers.infosits in a per-method rate limit that a per-message lookup would exhaust on a busy channel; absences and transient failures are cached too, so an unresolvable user costs one request rather than one per message.
It is off by default deliberately: the group roster persists whatever name the adapter reports, so turning it on changes what LibreFang stores about real people and not just what it displays (#7874) (@houko) - An agent in a shared Slack or Telegram group can now answer one person privately with the new
channel_dmtool, instead of having to broadcast a notice meant for a single member or drop it silently.
The recipient must already appear in that conversation's roster — someone the daemon has seen speak there — so the cross-chat dispatch guard that closed the #6117 leak stays closed: an agent still cannot address a platform id it has never met, or use one conversation to reach into another.
Slack lets a bot open a DM with any workspace member; Telegram and Discord bots cannot message someone who never started a chat with them, and that send now fails visibly rather than quietly reappearing in the group (#7874) (@houko) - Files and images uploaded into Slack now reach the agent instead of being dropped, so a member can hand over a campaign image or a clip in the channel where the work is happening and ask for something to be done with it.
The bot token is pinned to Slack's own file hosts, and downloads are gated by a size cap, an extension allow-list and per-channel switches so a busy channel can opt out without disabling uploads everywhere (#7812, #7087) (@houko) GET /api/memory/confignow reportsproactive_memory.session_scoped_recallandPATCHcan change it, with a matching switch in the dashboard's memory settings drawer.
The setting decides whether a memory an agent auto-memorized is recallable only from the conversation that produced it, so on a shared or public agent it is what stands between one visitor's turn and the next visitor's context — and until now the only way to read or move it was to openconfig.tomlon the daemon host, which is precisely what an operator driving LibreFang through its API cannot do.
The per-agent override stays inagent.toml, where every other per-agent key lives.
(#7870) (@houko)- Let a workflow step name an agent type —
agent = { type = "researcher" }— instead of an instance somebody had to register first, so a workflow you hand to a colleague runs on their machine without a setup step.
The step reuses the running agent of that name when there is one and otherwise spawns it from the template of that name under the canonical name-derived UUID, so the type keeps one identity and one conversation history across daemon restarts rather than accumulating a fresh agent per run.
Three keys can now address a step's agent and exactly one of them is accepted: a step setting bothagent_idandagent_nameused to be taken as valid and resolved by whichever the parser happened to read first, which is how a step ends up bound to an agent nobody wrote down and nothing in the run output says so.
Template loading also tells its failures apart — missing, unreadable, unparseable, and a manifest naming a different agent each report their own reason and pat...
v2026.8.19
Full Changelog
474 PRs from 5 contributors since v2026.7.31.
Highlights
- Security hardening — dozens of fixes closing SSRF vectors, path traversal in skill/channel IDs, XSS in canvas and OAuth callbacks, credential redaction gaps, and durable atomic writes throughout the daemon to prevent partial-state corruption.
- Managed configuration mode — new
managedmode locks provider config routes so self-hosted deployments can enforce a fixed LLM setup; pairs with opt-in model discovery and API key support for custom and local providers. - Long-form audio/video transcription — recordings are now processed in sliding windows and written directly to a file, removing the previous length cap on transcription.
- Non-blocking agent messaging and smarter task waking —
agent_sendis now non-blocking by default when called from within an agent turn; posted tasks automatically wake their assignee without requiring a separate trigger declaration. - Polish localization and i18n fixes — Polish (
pl) added as a supported language; Japanese, Spanish, and French error message translations restored and completed.
Added
- Scan the runtime container image for vulnerabilities with Trivy, on
mainand on every native release digest, so OS packages and the bundled Node.js / Python dependencies stop sitting outside the source and dependency checks.
In the release pipeline the scan sits between the per-architecturepush-by-digestbuild and the manifest publish, anddocker-manifestnow depends on it — no user-facing tag (:VERSION,:latest,:lts) can be created or moved onto a digest that failed the gate, and nothing downstream of the manifest (publish_arch_repo,deploy_fly,deploy_render,sync_aur_docker) consumes one either.
Each run publishes a job summary naming the platform digest, scanner and database versions, and every CRITICAL / HIGH finding with its package, installed version, fixed version, CVE, and severity; the raw JSON, the SARIF, and a machine-readable verdict are retained as artifacts and the SARIF is uploaded to code scanning under a per-image category.
The gate ships report-only: issue #6694 measured a 10-critical / 95-high backlog with Trivy 0.57.0, so arming it today would failmainon the first run.
The enforcement threshold is a single default — thefail-oninput of.github/actions/trivy-image-scan— that a maintainer movesoff → critical → highonce the backlog is remediated, and neither workflow overrides it.
Nothing is suppressed to achieve that: no--ignore-unfixedand no severity filtering, so the report is complete either way and only the pass/fail decision is narrowed to fixable findings.
A vulnerability-database download failure is kept distinct from a clean scan — refreshed in its own retried step, with the scan then running--skip-db-update— so a scanner outage fails loudly instead of passing as a finding-free image (#6694, #6712) (@houko) - Add a managed configuration mode so a deployment can own
config.tomlinstead of treating it as application state.
LIBREFANG_CONFIG_PATHrelocates the file — useful on its own, for a Compose bind mount or a ConfigMap mounted outsideLIBREFANG_HOME— andLIBREFANG_CONFIG_MODE=managedlocks it.
The two are deliberately independent: relocating a file is not a statement about who owns it, and inferring the lock from the path would hand a read-only dashboard to an operator who only wanted the file somewhere else.
The mode is read from the process environment and never from the config file, so a write through the API cannot unlock the very file it is being refused access to.
When managed mode is active, every API surface that persists deployment configuration answers423 Lockedwith{"code": "config_managed", "source": "<path>"}and leaves the file untouched — enforcement lives in the handlers rather than relying on a read-only mount, because a filesystemEACCESsurfaces as a 500 with an errno and tells an operator nothing about why.
GET /api/config/statusreports the mode, the source path, writability, a SHA-256 over the file's bytes, and its last-modified time, so the dashboard can present managed settings as read-only from server-supplied metadata rather than by attempting a save and reading the refusal back.
Boot-time schema migration no longer tries to write the migrated config back when the file is managed; it logs a single targeted warning instead.
That write previously failed against a read-only mount with nothing but awarn!, so the migration re-ran silently on every boot forever.
Mutable mode remains the default and is unchanged (#6695, #6717) (@houko) - Polish (pl) is now a supported UI language across the dashboard SPA, the backend Fluent error catalogue, and the webchat widget.
The channel bridge also emits a Polish failure suffix for tool-failure progress lines.
(#6696) (@leszek3737) - Opt-in live model discovery for custom OpenAI-compatible providers, via a
discover_modelsflag on the provider and a toggle in the dashboard's Add / Configure Provider dialogs.
Discovery was gated on a hard-coded id allowlist (ollama | vllm | lmstudio | lemonade), so a self-hosted endpoint registered under any other id was never probed: its model list stayed empty forever and the only recourse was to register every model by hand, or to squat the built-invllmid and override its base URL.
A provider that opts in joins exactly the paths a built-in local one already walks — the 60-second probe loop, thePOST /api/providers/{name}/testrefresh, and the live-model filter on/api/models.
The predicate ORs the flag with the id check rather than replacing it, so the built-in ids keep discovering regardless of the flag and an existing install sees no change.
PUT /api/providers/{name}/discoverytoggles it and persists the value into the provider's own TOML, so the opt-in survives a restart — for a provider you created, which is the case the feature exists for; on a registry-shipped file the boot-time sync still reverts any local edit (#6702, #6714) (@houko) - Run the Python SDK test suite in CI.
sdk/python/tests/held roughly 1900 pytest cases covering the HTTP client and every stdlib-only sidecar channel adapter — slack, discord, telegram, mastodon and the rest — and no workflow ran a single one of them, so the production code path for every sidecar channel shipped with CI fully green regardless of what broke.
Thesdk/prefix was already routed to the Rust lane, but only as an openapi codegen drift guard, which runs cargo and never pytest.
The new lane installs the package with itsdevextra and runs the suite on anysdk/python/**change, in under a minute (#6741) (@houko) - Teach a task-board trigger to fire on unowned work via
pattern = { task_posted = { assignee_match = "unassigned" } }.
Previously the only options were "every posted task" or a specific agent, so an agent that should pick up whatever nobody has claimed had to match everything and filter in the prompt.
The keyword matches both spellings of unowned that reach the event — an absent assignee and the empty string — because neither thetask_posttool norPOST /api/tasksnormalises the field, while both do reject an empty title and description.
A client that sends an empty assignee means "nobody", and a filter that only understood the absent form would silently ignore it.
(#6742) (@houko) media_transcribecan now transcribe a recording in bounded windows and write the transcript to a workspace file, which is what a recording longer than a few minutes needs to reach an agent at all.
Previously the tool transcribed whole files and returned the transcript inline, so two limits unrelated to file size decided how long a usable recording could be: a single transcription request is bounded by a wall-clock timeout that does not scale with the input, and the kernel spills any tool result over[tool_results] spill_threshold_bytes(16 KB by default) to the artifact store and hands the agent a stub instead.
Both are reached around ten minutes of speech, at roughly 2.5 MB of extracted audio — far belowMAX_AUDIO_BYTES, and further still belowMAX_VIDEO_BYTES, so no size limit is anywhere near being involved.
start_secandmax_secsbound the request to one window and the response carrieshas_more/next_start_secto walk the rest;out_pathwrites the transcript as UTF-8 and returns only the path, byte count, sha256 and a 200-character preview, following the contractweb_fetch_to_filealready established.
Windows starting at0begin a new file and later windows append, so repeated calls assemble one transcript without any of it passing through the agent's context.
Both mechanisms are needed rather than either alone: window size varies with how much was said, so a fixed window straddles the spill threshold instead of staying under it, andout_pathis what makes the outcome independent of that.
Callers advance by the produced window length rather than the requested one, read back from the Ogg granule position — a seek lands on a keyframe and a window overlapping the end of the recording is short, so an assumed edge drifts and eventually skips audio.
Consecutive windows are separated by a newline: a boundary lands mid-sentence by design and each window's transcript arrives trimmed, so concatenating them directly would fuse the last word of one window to the first of the next at every boundary.
A call that names neither window field keeps its previous behaviour exactly, including adding no ffmpeg pass. (#6748, #6773) (@nevgenov)
Fixed
- Escape every TOML control character when the dashboard serializes agent manifest strings, preserving carriage returns, tabs, and other control bytes as valid round-trippable TOML instead of producing a manifest the daemon cannot parse. (@TechWizard9999)
browser_read_pageno longer drops...
v2026.7.31
Full Changelog
58 PRs from 4 contributors since v2026.7.27.
Highlights
- API key security — Keys now support env/vault indirection and a hashed form, closing a hash-only WebSocket/terminal auth bypass; three additional authorization boundaries around plugin execution, MCP env values, and cross-user token refresh were also closed.
- Speech-to-text improvements — Language and prompt parameters now thread through STT, and video containers are accepted as input for transcription.
- Browser CDP attachment — Agents can now attach to browser-level Chrome DevTools Protocol endpoints via
Target.createTarget, enabling richer browser automation. - EveryAPI integration — Auto-detection of EveryAPI CLI credentials and new partner surfaces make connecting to EveryAPI faster and require no manual setup.
- Kubernetes deployment — A single-replica baseline with readiness contract and rootless restricted-PSS container support makes LibreFang deployable on Kubernetes out of the box.
Added
-
Add
exec_policy.full_mode_skips_approval, which decouples the two propertiesmode = "full"has always fused, so an operator can run unrestricted shell commands that still prompt for approval.
Fullwaived the globalapproval.require_approvallist forshell_execas well as skipping allowlist validation, so an operator who deliberately setFullfor one agent silently lost theirrequire_approval = ["shell_exec"]for it, with nothing on any surface saying so.
That coupling was deliberate rather than accidental, so the flag makes a documented decision overridable instead of fixing a bug: with the flag off,Fullwaives only command validation and[approval]decides who must confirm, exactly as underallowlist.
The default istrueand preserves today's behaviour on every existing install, and it is deliberately not flipped for two reasons that are load-bearing together:ApprovalPolicy::default()ships withshell_execinrequire_approval, andKernel::spawnpromotes any standalone agent whosecapabilities.toolscontainsshell_execor*and which declares noexec_policytomode = "full".
On a stock install this waiver is therefore the only reason ordinary agents run shell commands unattended, and a flipped default would prompt on every command rather than expressing a new operator intent.
Thesafe_bins_skip_approvalwaiver from #6000 is intentionally left unconditional: it is by its own name an explicit approval opt-out, whereasFullis a command-validation mode that never claimed to speak for[approval].
A per-user RBACNeedsApprovalstill forces the approval queue in either position of the flag, and the field is readable onGET /api/configalongside its neighbour.
Like the rest ofexec_policyit is baked into each agent's manifest at spawn / restore time, soPOST /api/config/reloaddoes not retrofit it onto already-running agents — kill the agent and let it respawn, or restart the daemon (#6594) (@houko) -
Add an EveryAPI connect action to the dashboard's Providers page, so registering the gateway no longer requires dropping to
librefang models connect everyapiin a terminal.
EveryAPI is not a built-in provider: until a registry entry exists it is absent fromGET /api/providersaltogether rather than merely unconfigured, so the Add picker — which lists what that endpoint already returns — could never surface it, and the dashboard had no path to it at all.
The picker footer now offers a connect action while noeveryapientry exists, opening a two-field drawer for the relay key and an optional gateway root.
It keys off the entry existing rather than being usable, because an entry with a missing key is already reachable through the normal configure flow and re-offering connect there would overwrite it.
The entry is registered with an emptymodelsarray on purpose:catalog_needs_initial_refreshincrates/librefang-api/src/everyapi_catalog.rsis true exactly when the provider is configured but has no live models, so the daemon'srefresh_if_missing_in_backgroundfetches/v1/modelsand/api/pricingand synthesises the catalog itself.
Duplicating that synthesis in the browser would mean a second copy of the pricing rules to keep in sync, and the gateway is not CORS-open to the dashboard in any case.
The provider id, display name, key env var and default gateway root are exported asEVERYAPI_PROVIDERalongside the mutation and documented as a cross-language contract with the CLI's constants, since a drift there would register a provider the daemon never refreshes (#6586) (@houko) -
Add
librefang models connect everyapi [--set-default], which registers an EveryAPI aggregating gateway as a custom LLM provider in one command, plus anEveryApiWiringCheckinlibrefang doctorthat reports whether such a gateway is present and which route is actually in effect.
EveryAPI's owneveryapi use <tool>injects environment variables and execs a child process, which does nothing for a long-lived daemon whose HTTP drivers readbase_urlfrom config rather than the environment — so the wiring belongs on the LibreFang side.
The command readsapi_baseandrelay_keyfrom~/.config/everyapi/credentials.json(honouring$XDG_CONFIG_HOME), fetches the gateway's live/v1/modelslisting, resolves model metadata from the builtin catalog (the gateway publishes ids but no pricing and mostly no context window), stores the key in~/.librefang/.env, and persists the provider through the running daemon when one is up or by writing~/.librefang/providers/everyapi.tomlwhen it is not; the key never enters the provider TOML, the terminal, or the logs.
A text model whose context window and output limit cannot both be resolved is skipped and named in the output rather than registered, because such an entry is discarded when the catalog loads and would otherwise vanish silently; entries with unresolved pricing are markedpricing_known = falseso budget math does not treat them as free.
Synthesised entries deliberately avoidModelTier::Custom:find_modelreturns the firstCustommatch immediately (#983) andmerge_catalog_filededupes on(id, provider), so aCustomgateway copy of a colliding id (claude-sonnet-5,claude-opus-5,gemini-3.5-flash) would have hijacked every provider-blind lookup and silently re-priced agents that never opted into the gateway.
--set-defaultwill not pick anopenai-response-only model, since those reject the non-streaming calls that compaction, proactive memory, the skill workshop, and web augmentation all issue.
The doctor check warns when an env route and a provider entry are live at once, naming both, because the effective gateway then differs per driver and neither surface mentions the other (#6583) (@houko) -
Source EveryAPI model metadata from the gateway's own public
/api/pricingfeed rather than reverse-looking-up ids in the compiled-in OpenRouter snapshot, and refresh the catalog on a TTL so the model list does not go stale.
The snapshot lookup guessed a vendor prefix fromowned_by, resolved only 7 of 18 models, and copied the upstream vendor's list price — which is not what the gateway charges.
/api/pricingis served with optional auth, publishescontext_windowplus the gateway's real per-token ratios, and converts asmodel_ratio * 2.0for input andmodel_ratio * completion_ratio * 2.0for output; that conversion is corroborated by both the gateway's own docs and itsbilling/quota.gosettlement path againstQuotaPerUnit = 500000.
Models billed per call carry no per-token price and are emitted withpricing_known = falseinstead of a bare0.0, which would have asserted they are free.
Output-token limits are the one figure neither gateway endpoint publishes, so the snapshot is kept solely as their fallback and the command reports which models borrowed one.
A neweveryapi_catalogmodule inlibrefang-apimirrors the existingopenrouter_catalogshape — TTL staleness check, background backfill, and a shared retry window — so a gateway that adds or removes models is picked up without re-running the connect command (#6583) (@houko) -
Add
librefang service install --systemon macOS, which registers a boot-time LaunchDaemon instead of the login-time LaunchAgent the command has always written.
The existing behaviour was that every platform got a per-user service, so a Mac that rebooted and stopped at the login window — or at the FileVault unlock screen — never started the daemon at all, which makes an always-on install impossible without hand-writing a plist.
--systemwrites/Library/LaunchDaemons/ai.librefang.daemon.plist, the directory launchd loads at boot before any login.
It requires root and specifically requiressudorather than a root login, because the job has to run as a real account andSUDO_USERis the only signal identifying which one; a missingSUDO_USERis an error rather than a silent default to root.
The generated job setsUserNameto that account,HOMEto its home directory andLIBREFANG_HOMEto~/.librefang, resolved from the passwd database rather than throughdirs::home_dir()— undersudothe latter returns root's home and the daemon would serve a state directory the invoking user cannot read.
The install also creates the state directory anddaemon.logand hands the whole tree to the target account, because launchd opensStandardOutPathbefore dropping privileges and the daemon would otherwise be unable to write its own log.
The handover is recursive on purpose:~/.librefangalmost always exists already by the time anyone reaches for--system, so chowning only the directory node would leave its contents on whatever uid created them, and a state directory previously written by asudo librefang startwould leave the LaunchDaemon...
v2026.7.27
release: v2026.7.27 (#6588) * chore: bump version to v2026.7.27 * chore(codegen): sync generated artifacts (openapi.json, SDKs, baselines) * docs(release): one sentence per line in the 2026.7.27 release article Per CLAUDE.md's prose-wrapping rule, break only at sentence boundaries in prose. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
v2026.7.21
release: v2026.7.21 (#6537) * chore: bump version to v2026.7.21 * chore(codegen): sync generated artifacts (openapi.json, SDKs, baselines) * docs: strip stray code fence and leaked assistant commentary from release article The committed file wrapped the whole article in an outer ```markdown code fence and appended a trailing block of AI-assistant meta-commentary that was never meant to ship, breaking the dev.to-style article render. --------- Co-authored-by: Evan <tonymo2048@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
v2026.7.11
Full Changelog
Stable release. First stable cut on the repaired release pipeline (#6433):
cli_pypinow publishes only for stable / LTS tags, so beta/rc pre-releases no longer fill thelibrefangPyPI project's 10 GB quota.- The desktop asset-cleanup no longer deletes the CLI binary tarballs, so the macOS/Linux/Windows CLI wheels publish to PyPI cleanly.
See the CHANGELOG for the full list of changes.
Install / Upgrade
Homebrew (macOS):
brew install librefang # CLI (stable) — official homebrew-core
brew tap librefang/tap # pre-release CLI + desktop app
brew install --cask librefang # Desktop (stable)CLI (Linux/macOS): curl -fsSL https://librefang.ai/install.sh | sh
npm: npm install -g @librefang/cli · pip: pip install librefang · cargo: cargo install librefang
Docker: docker pull ghcr.io/librefang/librefang:latest
Documentation · Discord · Contributing Guide
Full diff: v2026.7.10...v2026.7.11
v2026.6.29
Full Changelog
14 PRs from 4 contributors since v2026.6.26-beta.24.
Highlights
- Korean language support — full UI, CLI/TUI, and error message translations added (233 keys covered)
- ARM64 Linux packages — aarch64 binaries now published alongside x86_64 via AUR and the project's pacman repo
- Telegram setup resilience — the setup form stays available after a describe timeout instead of disappearing
- Codex CLI flexibility — Codex CLI can now be used outside of Git repositories
- Mixed-media message enrichment — coalesced batches with mixed content types are now correctly enriched on the debounced path
Added
- UI Korean translation (#6349) (@seungjin)
- Complete Korean error translations (43 → 233 keys) (#6353) (@houko)
- Add Korean (ko) translation for the CLI/TUI (#6356) (@houko)
- Publish aarch64 packages alongside x86_64 (#6334) (#6358) (@houko)
Fixed
- Bump pdf-extract 0.10→0.12 to patch lopdf RUSTSEC-2026-0187 (#6339) (@houko)
- Keep Telegram setup form available after describe timeout (#6345) (@pavver)
- Allow Codex CLI outside Git repositories (#6347) (@pavver)
- Enrich coalesced mixed-media batches on the debounced path (#6348) (#6351) (@houko)
Documentation, maintenance, and other internal changes
Maintenance
- Symlink legacy NDK binutils so vendored OpenSSL cross-compiles for Android (#6335) (@houko)
- Put NDK bin on PATH so openssl-src finds the legacy ranlib symlink (#6338) (@houko)
- Enable auto-merge instead of forcing --admin (#6340) (@houko)
- Publish AUR packages on release (#6334) (#6341) (@houko)
- Publish project-maintained pacman repo to R2 (#6334) (#6352) (@houko)
Other
- Fix[flake.nix]: Add perl to nativeBuildInputs (#6346) (@FrantaNautilus)
Install / Upgrade
Homebrew (macOS):
brew tap librefang/tap
brew install librefang # CLI (stable)
brew install librefang-beta # CLI (beta channel)
brew install librefang-rc # CLI (rc channel)
brew install --cask librefang # Desktop (stable)
brew install --cask librefang-beta # Desktop (beta channel)
brew install --cask librefang-rc # Desktop (rc channel)CLI (Linux/macOS): curl -fsSL https://librefang.ai/install.sh | sh
npm: npm install -g @librefang/cli · pip: pip install librefang · cargo: cargo install librefang
Docker: docker pull ghcr.io/librefang/librefang:latest
Coming from OpenClaw / OpenFang? librefang migrate --from openclaw (or --from openfang)
Documentation · Discord · Contributing Guide
Full diff: v2026.6.26-beta.24...v2026.6.29
v2026.6.26-beta.24
Full Changelog
10 PRs from 2 contributors since v2026.6.24-beta.23.
Added
- Add Ukrainian localization and extract hardcoded copy (#6312) (@pavver)
- Add AUR packaging for Arch Linux (#6314) (@pavver)
- Surface run params, errors, and one-click re-run (#6292) (#6324) (@houko)
- Allow a custom model id when editing an agent (#6318) (#6327) (@houko)
Fixed
- Disable redirect following on OAuth HTTP clients (SSRF + credential leak) (#6315) (@houko)
- Block separator-less secret env names from WASM guests (#6316) (@houko)
- Guard gc_sweep running_tasks removal with task_id (TOCTOU) (#6317) (@houko)
- Describe inbound images on the debounced channel path (#6321) (#6323) (@houko)
- Accept empty-recipient HMAC so bootstrap_peers can connect (#6330) (@houko)
Documentation, maintenance, and other internal changes
Maintenance
Install / Upgrade
Homebrew (macOS):
brew tap librefang/tap
brew install librefang # CLI (stable)
brew install librefang-beta # CLI (beta channel)
brew install librefang-rc # CLI (rc channel)
brew install --cask librefang # Desktop (stable)
brew install --cask librefang-beta # Desktop (beta channel)
brew install --cask librefang-rc # Desktop (rc channel)CLI (Linux/macOS): curl -fsSL https://librefang.ai/install.sh | sh
npm: npm install -g @librefang/cli · pip: pip install librefang · cargo: cargo install librefang
Docker: docker pull ghcr.io/librefang/librefang:latest
Coming from OpenClaw / OpenFang? librefang migrate --from openclaw (or --from openfang)
Documentation · Discord · Contributing Guide
Full diff: v2026.6.24-beta.23...v2026.6.26-beta.24