Skip to content

Releases: librefang/librefang

v2026.9.19

Choose a tag to compare

@houko houko released this 20 Sep 16:00
bc0f937

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 with p, 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_spawn gains a profile parameter 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_url no longer accepts a plain http:// 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_spawn gains a profile parameter 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.
    Passing profile = "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] enabled is 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.
    A ModelProfile binds 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 = true in config.toml and mode = "flexible" in an agent's [model] block.
    Per-agent constraints live in [model.router_override] — allowed_profiles limits the choice, cost_budget caps the tier, default_profile is the fallback, and fixed = true opts the agent out entirely.
    A fallback profile is re-checked against those same constraints, so a default_profile can never spend past an agent's cost budget.
    Configurable from all four surfaces: GET/PUT /api/agents/{id}/model_routing and GET /api/model-router/profiles on the API, a Routing tab on the dashboard's agent detail, an r editor on the TUI agent detail screen, and librefang agent routing / routing-set / routing-profiles on the CLI (#7757) (@DaBlitzStein)
  • Loop engineering for autonomous goal runs, opt-in per goal.
    A run ended the moment the agent wrote GOAL_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.
    Setting loop_engineering on a goal adds two judges that are not the worker.
    A verifier agent (verify_agent_id) reads each iteration's output and returns VERDICT: PASS|FAIL|NEEDS_REWORK; a rejection goes back to the generator carrying the verifier's stated reason, up to verify_max_retries rework rounds, and until the verifier passes the work GOAL_DONE does 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_DONE changes 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 with GOAL_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's pending/ queue.
    Turning a run's lessons into a skill an agent actually loads still takes a human librefang 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 own agent_spawn / agent_send tools, 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}/pause and POST /api/goals/{id}/resume.
    Until now stop was 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 in GET /api/goals/{id}/run in the new paused phase 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_secs on the goal itself, accepted by POST /api/goals and PUT /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 new POST /api/goals/{id}/pause and /api/goals/{id}/resume endpoints, 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_url makes "Propose to Registry" usable on GitHub Enterprise at all, where a compiled-in api.github.com left it with no workaround.
    commit_author_name / commit_author_email fix 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_prefix and a mode of fork or direct_push cover 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.
    main already colours a paused run yellow and ships tui-goals-phase-paused and tui-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, because s is 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...
Read more

v2026.9.14

Choose a tag to compare

@houko houko released this 13 Sep 19:06
57ad923

Full Changelog

203 PRs from 5 contributors since v2026.8.30.

Highlights

  • Goals system — a new goal command 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_template on 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}/start auto-spawns a disposable worker agent when none is set, and, when the goal's loop_engineering flag 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 configurable evaluator_model can judge goal completion instead of trusting the agent's self-reported GOAL_DONE marker alone, and GOAL_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 new librefang 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 /goal slash command in the Telegram and Slack sidecars (#6505) (@DaBlitzStein)
  • Add agent_spawn ephemeral 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-ephemeral is the HTTP entry point, and the dashboard's Quick Run drives it (#7875, #7903) (@DaBlitzStein)
  • Add write operations (POST/PUT/DELETE) to the /api/templates surface, 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 own agent.toml has always been spawnable-from and is listed alongside it.
    Only the first is writable, so every row carries an editable flag 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_create tool 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 same Workflow::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 the workflow-creator skill — 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 when VITE_SKILLHUB_REGISTRY_URL names 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.
    ModelOverrides carried no context_window at 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, and resolve_context_window honours 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, and librefang 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 goal command and a Goals tab in the TUI, both against the existing /api/goals surface.
    librefang goal "<description>" --agent <name-or-uuid> creates the goal and starts the run, --max-iterations caps it, and --watch polls 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, because POST /api/goals/{id}/start refuses 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 from GET /api/goals/{id}/run when 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's status, which is set to in_progress when 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's agent.toml carries operator-private data the registry validator does not check for — absolute workspace paths, api_key_env / base_url credentials on the model and every fallback, external workspace mounts, host-specific exec_policy, literal context_injection text, and instance metadata — none of which belongs in a public git history, where it stays.
    sanitize_for_publication returns a publishable copy with that removed and the portable agent-type half untouched; scan_for_publication independently reports what it found, with a bounded preview per finding, including in fields the sanitiser deliberately keeps, like system_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 destructures AgentManifest with 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...
Read more

v2026.8.30

Choose a tag to compare

@houko houko released this 30 Aug 00:28
fe015c6

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.toml is rendered into a ConfigMap and mounted read-only outside the PVC, credentials stay in Secrets and reach the daemon as environment variables, and a checksum/config annotation 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 value GET /api/config/status reports 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 new ephemeral: true mode — 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 same max_agent_call_depth counter agent_send and workflow_run already 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-ephemeral is 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 shared max_agent_call_depth.
    Resolving an agent type by name now also searches the writable agent-types/ store, which is where POST /api/templates and the agent_type_create tool 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_create tool, 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 next workflow_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-creator skill so an agent reaching for workflow_create knows 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-off agent_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-step session_mode (#7873) (#6934) (@houko)
  • channel_members can now report everyone a platform lists in a channel, not only the people who have spoken there — the Slack adapter walks conversations.members when SLACK_ENUMERATE_MEMBERS is 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 a source column on each roster row, because channel_dm authorizes 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 unlike SLACK_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 @handle through users.info instead of surfacing the raw U09… id, behind the new opt-in SLACK_RESOLVE_DISPLAY_NAMES knob.
    Lookups are cached per user id for SLACK_DISPLAY_NAME_TTL (six hours by default) because users.info sits 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_dm tool, 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/config now reports proactive_memory.session_scoped_recall and PATCH can 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 open config.toml on the daemon host, which is precisely what an operator driving LibreFang through its API cannot do.
    The per-agent override stays in agent.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 both agent_id and agent_name used 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...
Read more

v2026.8.19

Choose a tag to compare

@houko houko released this 18 Aug 23:04
05ca7ff

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 managed mode 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_send is 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 main and 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-architecture push-by-digest build and the manifest publish, and docker-manifest now 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 fail main on the first run.
    The enforcement threshold is a single default — the fail-on input of .github/actions/trivy-image-scan — that a maintainer moves off → critical → high once the backlog is remediated, and neither workflow overrides it.
    Nothing is suppressed to achieve that: no --ignore-unfixed and 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.toml instead of treating it as application state.
    LIBREFANG_CONFIG_PATH relocates the file — useful on its own, for a Compose bind mount or a ConfigMap mounted outside LIBREFANG_HOME — and LIBREFANG_CONFIG_MODE=managed locks 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 answers 423 Locked with {"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 filesystem EACCES surfaces as a 500 with an errno and tells an operator nothing about why.
    GET /api/config/status reports 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 a warn!, 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_models flag 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-in vllm id 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, the POST /api/providers/{name}/test refresh, 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}/discovery toggles 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.
    The sdk/ 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 its dev extra and runs the suite on any sdk/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 the task_post tool nor POST /api/tasks normalises 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_transcribe can 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 below MAX_AUDIO_BYTES, and further still below MAX_VIDEO_BYTES, so no size limit is anywhere near being involved.
    start_sec and max_secs bound the request to one window and the response carries has_more / next_start_sec to walk the rest; out_path writes the transcript as UTF-8 and returns only the path, byte count, sha256 and a 200-character preview, following the contract web_fetch_to_file already established.
    Windows starting at 0 begin 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, and out_path is 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_page no longer drops...
Read more

v2026.7.31

Choose a tag to compare

@houko houko released this 31 Jul 04:11
6faec4e

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 properties mode = "full" has always fused, so an operator can run unrestricted shell commands that still prompt for approval.
    Full waived the global approval.require_approval list for shell_exec as well as skipping allowlist validation, so an operator who deliberately set Full for one agent silently lost their require_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, Full waives only command validation and [approval] decides who must confirm, exactly as under allowlist.
    The default is true and 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 with shell_exec in require_approval, and Kernel::spawn promotes any standalone agent whose capabilities.tools contains shell_exec or * and which declares no exec_policy to mode = "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.
    The safe_bins_skip_approval waiver from #6000 is intentionally left unconditional: it is by its own name an explicit approval opt-out, whereas Full is a command-validation mode that never claimed to speak for [approval].
    A per-user RBAC NeedsApproval still forces the approval queue in either position of the flag, and the field is readable on GET /api/config alongside its neighbour.
    Like the rest of exec_policy it is baked into each agent's manifest at spawn / restore time, so POST /api/config/reload does 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 everyapi in a terminal.
    EveryAPI is not a built-in provider: until a registry entry exists it is absent from GET /api/providers altogether 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 no everyapi entry 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 empty models array on purpose: catalog_needs_initial_refresh in crates/librefang-api/src/everyapi_catalog.rs is true exactly when the provider is configured but has no live models, so the daemon's refresh_if_missing_in_background fetches /v1/models and /api/pricing and 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 as EVERYAPI_PROVIDER alongside 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 an EveryApiWiringCheck in librefang doctor that reports whether such a gateway is present and which route is actually in effect.
    EveryAPI's own everyapi use <tool> injects environment variables and execs a child process, which does nothing for a long-lived daemon whose HTTP drivers read base_url from config rather than the environment — so the wiring belongs on the LibreFang side.
    The command reads api_base and relay_key from ~/.config/everyapi/credentials.json (honouring $XDG_CONFIG_HOME), fetches the gateway's live /v1/models listing, 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.toml when 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 marked pricing_known = false so budget math does not treat them as free.
    Synthesised entries deliberately avoid ModelTier::Custom: find_model returns the first Custom match immediately (#983) and merge_catalog_file dedupes on (id, provider), so a Custom gateway 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-default will not pick an openai-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/pricing feed 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 from owned_by, resolved only 7 of 18 models, and copied the upstream vendor's list price — which is not what the gateway charges.
    /api/pricing is served with optional auth, publishes context_window plus the gateway's real per-token ratios, and converts as model_ratio * 2.0 for input and model_ratio * completion_ratio * 2.0 for output; that conversion is corroborated by both the gateway's own docs and its billing/quota.go settlement path against QuotaPerUnit = 500000.
    Models billed per call carry no per-token price and are emitted with pricing_known = false instead of a bare 0.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 new everyapi_catalog module in librefang-api mirrors the existing openrouter_catalog shape — 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 --system on 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.
    --system writes /Library/LaunchDaemons/ai.librefang.daemon.plist, the directory launchd loads at boot before any login.
    It requires root and specifically requires sudo rather than a root login, because the job has to run as a real account and SUDO_USER is the only signal identifying which one; a missing SUDO_USER is an error rather than a silent default to root.
    The generated job sets UserName to that account, HOME to its home directory and LIBREFANG_HOME to ~/.librefang, resolved from the passwd database rather than through dirs::home_dir() — under sudo the 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 and daemon.log and hands the whole tree to the target account, because launchd opens StandardOutPath before dropping privileges and the daemon would otherwise be unable to write its own log.
    The handover is recursive on purpose: ~/.librefang almost 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 a sudo librefang start would leave the LaunchDaemon...

Read more

v2026.7.27

Choose a tag to compare

@houko houko released this 27 Jul 11:41
244b0cb
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

Choose a tag to compare

@houko houko released this 21 Jul 09:36
7835a8c
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

Choose a tag to compare

@houko houko released this 10 Jul 18:03
7e2ac18

Full Changelog

Stable release. First stable cut on the repaired release pipeline (#6433):

  • cli_pypi now publishes only for stable / LTS tags, so beta/rc pre-releases no longer fill the librefang PyPI 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

Choose a tag to compare

@houko houko released this 29 Jun 07:01
8b1893e

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

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

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

Choose a tag to compare

@houko houko released this 26 Jun 07:17
d522fea

Full Changelog

10 PRs from 2 contributors since v2026.6.24-beta.23.

Added

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