Release Archive
This page keeps release and daily update history outside the README so the project homepage can stay focused on what nanoinfra is, what it can do, and how to start.
For 1.0.0 and later, read
CHANGELOG.md โ one line per
change, in Keep a Changelog form. This page is the long-form account, and it is the only record of
the 0.x line.
For tags and downloads, see GitHub Releases.
Highlightsโ
-
2026-08-31 ๐ Released v1.1.0 โ a turn shows what it cost, the usage store keeps one row per attempt, and a connector can arrive from the catalog. Eight issues, and the first four are one change told in four commits:
usage: dict[str, int]held four invariants by convention, and two of them it did not hold at all. The visible one was the partition โprovider_tokenswas set withsetdefault, so where a number came from survived as a key that happened to be present, and a reader of a cost figure could not tell which half they were looking at. The two that were absent are about the difference between not measured and measured as zero: cache was one scalar written in one place, and an absent dict key reads as zero at every consumer, so "this provider does not report cache" and "nothing was cached" were the same number.LLMUsageis a frozen value that validates all four at construction. Three defects fell out of the type that nothing had noticed before: a cache count present on one call and absent on another added to the one that existed, producing a cache-hit rate over a denominator that included calls with no measurement; two calls carrying 40k of context each read as a turn that carried 80k, and nothing ever carried 80k; and the over-budget finalisation path mutated the usage accumulator through its argument, which is the one path that runs when somebody is already looking at the number.token_calibrationis why this had to be a type rather than a tidier dict: it learns how far our tokenizer sits from what a provider charged, its guard was the caller's position in the code rather than the value, and readingreported_tokensmeans it can no longer be taught its own output. Then the numbers reached the thread. They had existed per call and per turn for as long as the tool loop has, and the only WebUI surface for any of it was a bucket per day in Settings;/statuswas the only per-turn figure anywhere, as text, for the last turn only. The assistant footer now reads3.2K in ยท 412 out ยท 87% cached ยท 4.1s, persisted beside the latency so a reloaded thread shows the number a live one showed. A~marks a total that includes an estimate, and the cache share appears only when the provider reported one โ the partition becoming visible to the person paying for it. The store's placement was the finding, not the plumbing. Three call sites reach a provider without going through the agent loop โ the WebUI title generation, the evaluator, and the Dream consolidation โ and anAgentHookonly fires for turns that went through it, so their tokens were charged by the provider, paid by the operator, and counted by nothing. The heatmap undercounted and nobody could see by how much. The observer sits on the provider's own retry loop now, where every physical call passes exactly once: a retried call is two rows, a cancelled attempt is recorded and re-raised, and "how many calls failed and retried, and what did the retry cost" has an answer for the first time. That also settled where the estimate lives, because a record emitted at the boundary with the estimate added later would be precise about the calls it could see and silent about exactly the ones it had to guess. The exclusions are the reason to keep 400 days of it: no prompts, no responses, no reasoning, no tool payloads, no provider error text, no session keys, and finish reasons and error kinds normalised to a coarse set before they are written. A test asserts the schema has no column content could go in. Upstream shipped the same store with no migration, dropping every day row a deployment had; ours seeds from the old JSON at startup, writes zero for what was never measured, and renames the file rather than deleting it, because those numbers were the only copy. And the connector marketplace, whose last section was unbuilt. The part that mattered most was not in the original design: a package declares its ownbaseUrl, so one declaring Google scopes andbaseUrl: https://evil.examplereceives a live Google token โ and a confined host process forwards it just as obediently as the executor would, because the token is in the request the manifest asked for rather than in the process's memory. Landlock does not stop an outbound HTTPS call; that is the one thing a connector host exists to do. What refuses it is a credential bound to the hosts it may address, checked at activation with both hosts named. The format isconnector.jsonand nothing importable, because importing amanifest.pythis deployment did not write is running it โ and the check runs on every load rather than only at install, because a directory is a directory. An unknown key is a refusal rather than an ignored field, a declared dependency is refused rather than dropped, and both formats produce the sameConnectorPlugin, so the gate, themaxClassceiling, the per-class token and the audit record needed no changes at all. A marketplace package's call runs in a fourth confined process (nanoinfra-connector, uid 1004) whose group holds the executor and not the agent, and which re-reads the package to refuse a URL that is not its own. The catalog side ships in skills-server:connector.jsonselects a third package kind, the stored kind says what a reader is installing, and the admin review screen now shows the capability class of every operation, the hosts a token could reach, the scopes it would carry, and aruns codemark on an Agent Plugin's MCP servers โ which nothing surfaced before. -
2026-08-31 ๐ง Released v1.0.6 โ a secret record takes the directory's group, not the writer's. One fix, found on a real deployment one release too late, and worth writing down because of why the test missed it. The 1.0.5 work had already fixed the mode: a record now follows the directory rather than a hardcoded
0600, so the gateway can read metadata for a file the executor wrote. The group was still the writer's. A record landed asnanoinfra-exec:nanoinfra-execwhile the gateway is innanoinfra-ipc, which is a group-readable file in a group the reader is not in โ so a create through the executor succeeded and the very next listing answeredEACCES, the same symptom as before with a different cause underneath. A directory carrying setgid hands its group down, and the throwaway container 1.0.5 was verified in had one: that is exactly why the test passed and the deployment did not, because the demo'ssecrets/predates that entrypoint. The store now sets the group explicitly when the directory shares with one, and a refusedchownis logged rather than failing a write whose bytes already landed. The test records which gid is asked for rather than performing the change, because a real gid change needs two accounts and the gid is the part that was wrong. Verified on the live deployment: create, list, update, resolve, delete. 8,097 Python tests, BasedPyright strict at 0 errors. -
2026-08-31 ๐ Released v1.0.5 โ the Secrets page works on a container deployment, a connector authorises itself in the browser, and four upstream fixes that were holes here too. Six issues, and the first one had been shipped:
secrets/belongs to the executor account with a group read and no group write, socreate_webui_secretfailed with a raw permission error on every deployment with the privilege split โ every container deployment. A plainpip install, where one account owns everything, was unaffected, which is why nobody had reported it. The mode is not the bug: it is the reason a compromised agent can enumerate credentials and cannot replace one, so widening it was the ten-minute answer and the wrong one. The write crosses the boundary instead โ protocol version 6 carries a third request kind, the gateway encrypts because it holds the key, and the executor writes ciphertext it cannot read. Three verbs and no read. The WebUI and the agent share a process, so "the WebUI may write credentials and the agent may not" cannot be a uid boundary here; it is a boundary in the import graph, andtests/agent/test_secret_write_isolation.pyenforces it the way the redaction test enforces reading. Two defects in that work were found by running it in the published image rather than by reading it: a field namedkindcollided with the wire's own envelope discriminator and went missing on every frame, and a hardcoded0600wrote a file the gateway could not read back โ so a create succeeded and the next listing raised. The mode now follows the directory's. Connector consent completes in the browser. Activatinggoogle-calendarby hand took six steps and four were ours: carrying the?code=back, storing two secrets, writing the config block, restarting. What was missing was never a button but a record that a consent is in flight โ the PKCE verifier, the state, the redirect used, the scopes asked for โ which on the demo lived in a file under/rootinside the container, and that is exactly why a person carried the code. Two routes now: one starts the consent and answers with the URL, taking the client id and secret in a header because a query string reaches an access log; the other receives the redirect on this deployment's own origin, exchanges the code, stores both secrets and writes the credential and the activation. The callback answers without the API token, because Google redirects a browser carrying a cookie and no bearer โ thestateauthorises it, single-use and expiring, and one that matches nothing answers 404 and records nothing. It writes the activation too: an earlier draft leftconnectors.activeto a person on the argument that enabling is a decision a reviewer should see in git, and that argument lost to the requirement, because a flow ending in "now open config.json" is the thing being removed. What the boundary protects is unchanged โ no tool reaches these routes. The isolation test shaped this one as well: the first pass stored the client secret and read the plaintext back for the exchange, so the secret now lives in the in-memory record and both secrets are written only after the exchange succeeds, which means a failed consent leaves the deployment exactly as it was. The tools reload without a restart. Registration ran once at boot, so activating a connector afterwards leftconnectors listsayingactivewhile the running agent held no such tool and answered a calendar question by listing cron jobs โ anddocker compose up -dafter a config edit answersRunningand changes nothing, so an operator had done the documented thing and the tools were still absent. A reload travels as one runtime-control message, replaces stale instances and removes the ones config no longer activates, and the row raises a banner when what config activates and what the process registered disagree. Four upstream fixes were holes here too, each verified by reading our source rather than a commit subject: a Slack file download followed any URL the workspace handed it, redirects included, with no SSRF validation and no DNS pinning โ while the three helpers it needed were already used one directory away; the no-tools request path called the provider with no timeout while the tool-carrying path wrapped the same call, so a stall held the session lock forever and looked like a session that quietly stopped answering; a consolidation the model truncated (finish_reason == "length") was accepted as history and took whatever it had not reached with it; and the archive prompt paid tokens to re-write facts the system prompt already carried. Two more of ours, found in the same pass:find_filesandgrepbounded their results and nothing about their scan, and ran the walk inside an asyncexecuteโ so one tool call reading a big or slow filesystem held the event loop for every session on the gateway; both now carry an entry budget and a wall clock, and a partial answer says it is partial. And a cron job froze the workspace one person's turn resolved โ an absolute path naming their identity directory โ intojobs.json, echoed back by the automations payload; nothing read it back, because a later run resolves its workspace from the session file, so it was dead weight with a leak attached. Also:read_sessionrefused a blank query instead of reading the latest, and refused*with no explanation of why a literal-substring filter is not a glob; the session summary could appear twice in one prompt; a WebSocket listener that died left the gateway alive and deaf, and now reports readiness and rebinds; and the confined helpers were started with no config path, so a gateway started with--configran its executor against another instance's gates policy. Verified against a real Workspace domain and in a throwaway container from the published image. 8,096 Python tests and 1,276 WebUI tests, BasedPyright strict at 0 errors. -
2026-08-30 ๐ Released v1.0.4 โ data connectors: a kind whose operations each declare a capability class. An MCP server can reach the Google Calendar API today, and it cannot tell the gate what its tools do: an MCP tool declares nothing, so
capability_class_of()resolves every one of them to the fail-closedmutate.remote, a listing and a send land in one class, and the standing grant that lets an automation read your mail lets it send mail. A connector declares a class per operation, and a manifest that classifies aPOSTas a read is refused when it loads โ which is what makes the declaration worth trusting.google-calendarships first:list_events,list_calendarsandget_eventarereadand run unattended;create_eventismutate.remoteand asks. The call is performed in the executor, and the deciding vote was this repository's own isolation test, which walks every module the agent process can load and fails onresolve_plaintextโ it failed on the first pass of this work, correctly, because a refresh token in the gateway is a standing key to somebody's account sitting in the process the model steers. So protocol version 5 carries a second request kind: the agent submits a connector name, an operation name and the arguments, and the method, the path, the class and the scopes come from the installed manifest. A frame cannot describe a call the package never declared and cannot relabel a write as a read. That move is also what makes an interactive write possible rather than merely refused โ the executor renders the exact request, body values included, suspends the action, and the approval digest binds those bytes, so what a person reads is what runs. A standing grant can now name a connector:StandingGrantmatched hosts and commands, and a connector call has neither, so its write wasmutate.remotein an unattended turn with no grant that could ever match it.{"connectors": ["google-calendar"], "operations": ["create_event"]}fixes that with the same exact-name semantics the other dimensions have, and a grant names one kind of action or the other โ a config that writes both fails to load rather than guessing what "and" meant. The credential binding is the grant: a connector resolves the credential config names for it and nothing else, so one OAuth flow can serve four Google connectors without a package naming its own peers, and the token is minted per action for the scopes that class declared โ so a read receives a token that cannot write, and a Docs connector sharing a credential that holdsgmail.sendreceives one that cannot send mail.maxClasscaps what a package may offer whatever its manifest says, and every mismatch โ a missing scope, an operation the package lacks, a ceiling that leaves nothing โ is refused when the connector activates with both halves named, rather than at 03:00 in a run record. Consent is an operator at a browser:nanoinfra connectors authorize google-calendarruns the loopback flow with PKCE, stores the refresh token and the client secret, and prints their ids and neither value;--manualtakes a pasted redirect URL, for a shell with no browser. An authorisation the agent could start would be an authorisation nobody performed. Settings โ Apps โ Connectors says the posture: who it acts as, when the token last refreshed, what the gate answers for each class in both contexts, and which scopes the consent actually granted โ and Test performs one declared read through the executor, which is the only thing that tells a never-tried connector apart from a revoked one. No enable toggle, no Connect button, no data browser: activation isconnectors.activein config, consent is a person, and the surface for data is the conversation.@calendar:pins a calendar in the composer, refreshed by the picker and cached, resolved against that cache at send time so a listing never lands on the path of every message; the reference names the call the id belongs to, because a pinned id nobody knows what to do with is decoration. Also, and found by running it: the confined helpers were started with a socket and a workspace and no config path, so a gateway started with--configran its executor against another instance's gates policy, its fetcher against another instance's web settings and its MCP host against another instance's list of programs to start; a connector'sSKILL.mdwas shipped in the wheel and read by nobody; and a manifest field default was ignored unless config repeated it. Verified against a real Workspace domain rather than reasoned about: the read returned 200, the write rendered its request and waited for an approval on a second path, the picker listed five real calendars, and the token exchange reported Google's own words when the client was wrong. 8,040 Python tests and 1,276 WebUI tests. -
2026-08-29 ๐ฉน Released v1.0.3 โ no behaviour changes; the test that pinned the old dependency policy now states the new one. v1.0.2 moved four transports into the base dependencies, and
test_optional_dependency_metadata_for_enablestill asserted they were extras โ a test doing its job, which is to fail when a policy changes without anybody saying so. The assertions now name the four as base and keep pinning the exact contents of thebedrockandapiextras, so the next change to either has to be deliberate. The package behaves identically to 1.0.2. -
2026-08-29 ๐ง Released v1.0.2 โ the published image can reach a server.
asyncssh,ansible-runner,boto3andaiohttpwere optional extras, and the container image installs no extras, so a deployment that pulledghcr.io/nanoinfraorg/nanoinfrahad the server inventory, the executor, the gate and no transport to any of it โexecute_on_serverfailed on an import. The four are base dependencies now, because a transport is not an optional feature of an agent whose purpose is reaching machines: the extras stay as compatibility aliases sopip install nanoinfra[servers]keeps working and installs nothing new.apiandserversare the aliases;bedrock,azure,langfuseand the channel SDKs are still real extras, and still optional. -
2026-08-29 ๐ฉน Released v1.0.1 โ no behaviour changes; the tagged tree passes strict typing. v1.0.0 shipped correct code and a
cast()that narrows nothing, which BasedPyright rejects in strict mode (reportUnnecessaryCast). A cast does not exist at runtime, so this package behaves identically to 1.0.0 โ the release exists so the newest tag is a tree that passes every check rather than one whose failure a reader has to look up. Everything v1.0.0 describes is unchanged. -
2026-08-28 ๐ชช Released v1.0.0 โ a person who signs in gets their own workspace, their own sessions, and a way out. Identity already worked and reached nothing:
trustedProxyAuthverified an assertion,gates.approverscompared the name, and storage was one directory everybody shared. The switcher offered every workspace under the root to whoever asked, the sidebar listed every session on the deployment, and any session key reached any transcript. A verified identity now turns the workspaces root into that person's own directory. That is the entire mechanism, and it is deliberately not a second containment gate: the switcher lists what is under the root, a client may name a workspace under the root, and the access mode stays the operator's โ narrowing one input narrows all three, so there is nothing to keep in agreement with anything. A scope naming another person's directory is refused with 403 and not corrected, because a client that asked for it deserves to be told, andthat workspace is not yoursreplaces the shared-root advice to widentools.workspacesRootโ advice an operator might have followed, putting everyone back into one directory. Storage keys on(issuer, subject), never on the address. An address is mutable: a rename would orphan a directory and a reassigned Workspace address would inherit one, and inheriting somebody's files because you were given their old email is not a cosmetic bug. The directory is a truncated digest of that pair โ stable, opaque, filesystem-safe โ andworkspaces/.identities.jsonrecords which identity was last seen behind each one, so the disk stays legible to an operator without asking the gateway.workspaceKeyClaimnames the claim and defaults tosub; Google issues one per account per client, Dex builds one from the userID and the connector, and a token that verifies without it is admitted and shares the default, because refusing entry over a storage detail is the wrong answer. A personal workspace is seeded like any other, through the samesync_workspace_templatesa hand-made one goes through:AGENTS.md,HEARTBEAT.md,SOUL.md,USER.md,memory/andprompts/, and only on creation, so a second sign-in never overwrites an editedAGENTS.md. The store directories stay out and are never copied โsecrets/duplicated per person would be the worst of three options, and the credential store belongs to the executor's own workspace, which it never left. Sessions belong to whoever started them. The scope function that already ran on every WebUI chat records the workspace directory (not the subject claim, which in a session file is a subject claim in whatever a route later hands a client), and three cases decide who may see one: a caller with no identity sees the sessions with none, which is every deployment without a proxy behaving as it did; a caller with an identity sees their own; and a session with no identity is not shown to a caller who has one, because it predates them or belongs to the shared posture. Five routes read a session by key, so the check runs once in the dispatcher before any of them and answers 404 rather than 403 โ a 403 tells the caller the session exists, and the caller asking is the one person who should learn nothing from the difference. Attaching over the socket is guarded too, or it would be the way around the 404: the frames of a live turn would simply arrive. The page finally says who you are. The sidebar names the person a proxy asserted and offers Sign out beside it, both only when they are real: no assertion means no person to name, and nosignOutPathmeans no way out to offer, because the session is the proxy's cookie and a button that does nothing is worse than none. That field is a path and the schema refuses a URL. Settings โ Identity gained the claim that keys storage, the workspace this caller actually writes in, and whether it is theirs or shared โ and the System row that used to report the deployment's configured workspace to a person whose files go elsewhere now reports theirs. The container image is published.ghcr.io/nanoinfraorg/nanoinfra, on every version tag,latestplus the minor and the exact version,linux/amd64โ the deployment where the kernel enforces the privilege split was until now the one with the most setup.examples/auth/runs behind Caddy too, and writes down three details that do not fail loudly:forward_authkeepscopy_headersinside ahandle_responseit generates, so your ownhandle_responsefor the 401 replaces it and the verified token is dropped;handlesorts directives by Caddy's order while that block holds two proxies whose order is the point; and{host}drops the port, which is invisible on 443 and wrong in a lab. The example's honesty test reads both shapes now, merging base and overlay for the Caddy one โ a guard reading an overlay alone finds no secrets to check and reports that as a pass. Verified against that stack with two identities rather than reasoned about: two directories at mode 700, each person's switcher listing one workspace, 403 for the other's and for the shared default, 404 for the other's session, and the index naming both people. 7,928 Python tests and 1,265 WebUI tests. -
2026-08-25 ๐๏ธ Released v0.17.0 โ a Workspaces explorer in the WebUI, and a workspaces root behind it. Browsing a workspace was not possible from the browser:
list_diris an agent tool, and the only file route was a session-keyed text preview, so an operator could read a file the agent had named for them and could not find out what else was there.file_browser.pyis a sibling offile_preview.pyand takes its rule verbatim: containment is unconditional and does not readtools.restrictToWorkspace, because that setting governs the agent's own file tools while this decides what an authenticated browser may enumerate โ reading one setting to answer both is how a relaxed tool restriction becomes a remote directory walk of~/.nanoinfra/config.json. Every path goes through one gate withstrict=True, so a symlink inside the workspace cannot name a target outside it; one that tries is listed and marked rather than hidden, because a name that silently vanishes is more confusing than one that says why it cannot be followed. It is a file manager, not a viewer: the tree expands in place and keeps its listings when collapsed, right-click gives rename, delete, download, new folder and upload, and dragging a row onto a folder moves it. Every mutation takes(parent, name)rather than one path, and that shape is the whole defence for a symlinked entry โ the resolver must follow links, so resolving<dir>/linkhands back the target, and a delete built on that would remove what the link pointed at. A non-empty folder needsrecursivesaid out loud, and the client never says it speculatively: the server answers that a tree is involved and the second dialog says what that means. Folder upload, reviewed before it is written. A dropped folder is not inDataTransfer.filesat all, so it is walked throughwebkitGetAsEntryโ andreadEntriesanswers at most 100 entries per call, which is the classic way a folder upload silently loses everything past the hundredth. What was collected is then shown as a tree with each folder's file count and size, and anything can be left out and put back;.gitstarts left out, because 116 files of object database is not what anyone means by "upload this project". Files up to 100 MB are sent in 8 MB parts and assembled server-side: the old 20 MB ceiling was a WebSocket frame size leaking into the feature, and past it the socket closed with code 1009 instead of answering โ which left a real upload stuck at0/134with nothing on screen. The target is resolved once, on the first chunk, so a later chunk cannot redirect the bytes already written.tools.workspacesRoot(default~/.nanoinfra/workspaces) is where workspaces live and the boundary a client may name one inside; the configured workspace is allowed wherever it sits, because config is git-reviewed and widens deliberately while a path from a browser does not. A fresh install now gets~/.nanoinfra/workspaces/default, and an older one is moved there on next start with itssecrets/,diagrams/,servers/,skills/,triggers/andmemory/โ every guard in that migration prefers doing nothing over doing half, and a config rewrite that fails moves the workspace back. Also: saved diagrams update live in the browser instead of going stale until a reload; the file preview no longer reloads itself on every approvals poll, can be resized and can soft-wrap long lines, and its breadcrumb navigates; a paste too large to be a message becomes apasted-1.txtattachment instead of a wall of text in the conversation. Verified: ruff clean, BasedPyright strict at 0 errors, and 1,253 WebUI tests. -
2026-08-24 ๐ฉน Released v0.16.3 โ supersedes v0.16.2, whose test suite failed. The behaviour that release shipped is correct and unchanged here; two statements in the executor's entry point were in the wrong order, and one test that had every right to fail said so. The executor installs its log sink before it sets its name. The child configures loguru as the first statement under its
__main__guard andset_process_namehad jumped ahead of it โ and the order is not cosmetic: until that sink is installed, loguru prints the locals of every frame in a traceback, and this process holds a resolved command, a decrypted credential and the text under scrub, with its stderr going to a file in the run directory. So the sink goes first and the name waits one statement, which also puts its own debug line in the sink that was just configured. The test that caught it no longer breaks on a comment: it compared a source literal, so a comment placed inside the guard failed it while the guarantee was intact. It parses the guard now and asserts the first statement is that call, with a message that says what the ordering is for โ and the new form was checked against a deliberately broken entry point, so it is not vacuous. Everything v0.16.2 described still holds: the four processes of a split deployment answer togateway,exec,fetchandmcp, set withprctl(PR_SET_NAME)and no C extension, the names kept bare because the accounts already carry the project andTASK_COMM_LENleaves fifteen bytes. Verified:tests/gatesandtests/utilsat 1,437 passing, ruff clean, basedpyright clean in strict mode, and the process tree read out of a container built from this tree. -
2026-08-24 ๐ท๏ธ Released v0.16.2 โ the four processes of a split deployment answer to four names.
docker topon the demo, before this: onenanoinfraand threepython. The command line does carry the role โ each helper ends up aspython -m nanoinfra.gates.executor --socket โฆโ butcommis the name the short listings print and the onepgrepmatches without-f, sops -o comm,topandhtopshowed the same word for three processes running under three different accounts. In a deployment whose whole point is that those accounts are not each other, that was the one listing that could not tell them apart.prctl(PR_SET_NAME)sets that name and needs no C extension, which is why this is not the upstream approach:setproctitleexists to rewrite/proc/pid/cmdline, which does need C because it overwrites the process's own argv memory โ and the cmdline was not what was missing. One ctypes call, in a codebase whose confinement is already 914 lines of them, and no new dependency in a tree we audit. The names are bare โgateway,exec,fetch,mcpโ because the accounts already carry the project:ps -eo user,commprintsnanoinfra-exec exec, so a prefix would spend the budget on a word already on the line. And there is a budget:TASK_COMM_LENis 16, so fifteen bytes and a terminator, andnanoinfra-gatewayis seventeen. A name over the limit is refused rather than truncated, because acommcut mid-word names the wrong thing, and a test checks every shipped name against the limit and records why the prefixed form is not among them. Each helper names itself under its__main__guard rather than insidemain:confinement.mainexecs the role module and an exec resetscomm, so the name has to be set after the last exec โ and a test that callsmainin process must not rename the test runner, which is whereconfigure_child_loggingalready sits for the same reason. Verified in a container built from this tree rather than reasoned about:commreadsgateway,exec,fetch,mcpunder their four accounts, nopythonis left in the tree, and every command line is unchanged. Linux only, a no-op elsewhere. -
2026-08-23 ๐ Released v0.16.1 โ two faults the running demo produced, both invisible at boot and both arriving hours later on their own. Every session latched at 03:04 on a gateway that had reported the audit log readable at 22:17, and nothing changed in between except the UTC date: the first record of a new day creates that day's segment, and a fresh file takes its creator's primary group โ the executor's โ at mode 640, while the agent is the account that reads this log to restore latches. EACCES,
restore_latchesfailed closed as it must, and every gated action then waited for an operator with nothing to clear, since no denial had been recorded at all. Yesterday's segments were fine because a boot chowns what already exists; the file rotation creates hours later gets nothing._DIR_MODEasks for setgid, and for the second time the same measurement says it is not enough โchmod 2750on that directory reads back as 750, as root, on a container volume, exactly what the job store found when overlayfs droppedS_ISGIDon a copy-up. A bit the filesystem discards is not a mechanism, so the process that creates the segment sets its group, which an owner may do for any group it belongs to; the directory's group is the source of truth, so an operator who opened the log to an audit group keeps it and a single-uid host changes nothing. That warning had also appeared every fifteen seconds for an hour naming one file โ reported once per distinct failure now, and recovery is reported too, because a warning that simply stops reads exactly like a gateway that stopped trying. Second: a table replayed from history was missing its first column โ same conversation, same table, whole when it arrived live and headerless on a reload, with the long names showing only their tails.content-visibility: autois not only a paint optimisation: it implies paint containment, which clips descendants to the box carrying it, and that box was the display unit at the width of the reading measure while a table deliberately reaches past it with a negative inline-start margin. It showed only on replayed messages because those are the deferred ones โ a live arrival renders eagerly, never takes the class, and the deferral is sticky for the life of the page. A unit is now as wide as the widest thing it may hold, with the measure applied by a wrapper inside it, so the geometry no longer depends on whether a unit was deferred โ which is also what keeps the scroll anchor still when a deferred unit first paints. Verified: ruff clean, basedpyright clean in strict mode,tscand eslint clean,tests/gatesat 1,115 and the WebUI suite at 1,175, six of them new โ one across a segment rotation, one pinning the geometry invariant that broke. -
2026-08-22 ๐ณ Released v0.16.0 โ the confined container is the deployment this release repaired. Ten changes, every one found by running the split-privilege image at demo.nanoinfra.org rather than by reading it: faults a single-uid host cannot express, each reported as something else. Secrets โ the store belongs to the executor at mode 700 and the agent may not read a credential, which is the design; the bug was the swallowed refusal.
Path.globanswers aPermissionErrorwith an empty iterator andis_file()answers it with False, so the Secrets page said "no secrets yet" about a store holding an SSH key, fetching one answered 404, and a server that references a credential read as having none. The refusal travels now, the routes answer 409 with the reason, and metadata is a group read โ safe only because of the other half:setprivpasses the environment through, so the agent account heldNANOINFRA_SECRETS_KEYwhether anything used it or not and the file mode was the whole separation. The agent is exec'd without it. Sockets โ the group now belongs to the process that binds the socket, because the entrypoint's chown could not work: the supervisor waits for a socket at the path, not for this run's socket, so after a restart the chown lands on the previous run's file and the executor then unlinks it and binds a fresh one carrying its own primary group. An unreachable scrub socket withholds every persisted transcript; an unreachable execute socket fails every remote action. The fetcher and the MCP host survived the first fix because its test named three files โ it discovers its subjects now, by scanning for the calls that bind. The job store is the one directory both accounts write and the entrypoint had never prepared it, so every remote action failed after the gate permitted it; a setgid directory is the usual answer and is not enough, because overlayfs dropsS_ISGIDwhen a chmod copies an image directory up, so the store sets the group on the record instead. A transcript that would not render โwithheld_mappingreplaces every string value and an event's name is a string, so each record was withheld down to its owneventfield and a 5 MB transcript replayed as an empty chat; structure is kept now, values are still replaced, and a replay that finds nothing falls back to the session history, which recovers the files already damaged. Web search reaches the backend it is configured to call: a self-hosted SearXNG answers on 8080 and no default allowlist holds 8080 โ only the port comes from that URL, since a port allowlist that named a host would be a second, disagreeing opinion about the destination. And the fifth Landlock behaviour this codebase records: a rule on a file binds to that file's inode, so the atomic replace insave_configinvalidates it โ reproduced in a container, inode 12604338 before the save and 12606371 after. The fetcher reports that once, in a sentence an operator can act on, and stops retrying, since none of the retries could succeed; a web settings change now reportsrequires_restart, because a confined fetcher serves the old provider until it restarts. The sidebar footer points at three destinations in the order a reader wants them: nanoinfra (docs) v0.16.0 โ the name to the site,docsto the documentation, the version to the notes for this exact build. -
2026-08-21 ๐ท๏ธ Released v0.15.4 โ the running build is visible in the sidebar, under Settings. The version was only there after opening Settings; it belongs where a reader looks when they want to know which build answered them. A released version links to its own release notes; a local build does not, because a source checkout reports something like
0.16.0.dev3+g1a2b3c4and a link built from that sends the reader to a page that does not exist โ so anything that is not an exactx.y.zstays plain text, which still answers "which build is this?". Nothing renders before the settings payload arrives or on a gateway too old to send one, and the collapsed rail is left alone at 56 px, where every other label is hidden too. The line sits on the action row's own horizontal padding, so it starts on the same vertical edge as the gear icon and every nav icon in the rail โ the first attempt was 4 px left of that edge, which is enough to see and not enough to explain. Verified: 8,720 Python tests, 1,169 WebUI tests, basedpyright clean in strict mode, ruff and eslint clean, andtscclean. -
2026-08-21 ๐งช Released v0.15.3 โ an automation tells you what it needs before its schedule does. Creating a scheduled automation now runs it once, immediately, with every gated tool forced to preview โ nothing executes โ and reports what a scheduled run would meet: each action, whether the unattended policy permits it, and the standing grant that would permit a refused one. Why a rehearsal and not analysis: an automation is prose, so its commands are not in its text โ the model composes them when it runs โ and nothing can enumerate them by reading the record, so the instrument is a run, done while the operator is still in the conversation rather than at 03:00. The preview now answers the gate: the executor returned from its preview branch before the gate ran, so a preview said what would run and stayed silent on whether it would be permitted; both evaluations are pure and documented safe before a credential resolves, so
ExecuteResponsenow carries the outcome, the reason, the grant that matched, the scope, and the two halves of the grant that would permit it โ resolved hosts, never the label, and the exact command โ plus the credential class, because a permitted command still dies at the secret and an operator who fixes only the command cell meets the same failure one layer down.execute_on_serverdefaults todry_run=true, so that answer now arrives without anyone asking for it. And the trap that came with it: a previewed decision is never written to the audit log as adeniedrecord, becauserestore_latchesrebuilds latch state from exactly those records โ so recording the hypothetical would mean that asking what the gate would say latches the session and blocks the automation the question is about, across restarts. The test asserts both halves, since one that only asserted an empty latch set would pass on an executor that records nothing. Beyond the grant it reports the shadowing matrix cell in the gate's own words and the latch, which outranks every grant because the gate is not consulted at all while it holds; and it keeps two distinctions โ a rehearsal that could not complete is an error and not a refusal, or a provider outage would disable an automation, while a reference that no longer resolves is a refusal about the automation, because no grant fixes a deleted server. A refused automation is saved disabled with the finding attached: the authoring work survives and nothing sits enabled while certain to refuse. Re-running costs a model turn, so an edit only triggers one when the message, references or skills change โ renaming or rescheduling does not change which commands run. The card is deliberately not the approval card: an approval means an action is suspended and answering releases one action; a commissioning card means nothing is running and this is what the automation does on every run, so answering writes a standing permission. Hence Rehearse and Grant it, and no allow once โ no action is suspended. What the promotion refuses is the design: it takes no grant from the request, because a grant the caller could name would be a grant the caller chose, so it writes only what a rehearsal found and refuses an automation with no refused finding, a finding no grant could satisfy, an empty host or command list, a context other thanunattended, and a stored proposal that is not a grant โ a hand-edited record costs the promotion rather than buying a wider one. Promoting twice writes one grant (two grants for one permission means revoking it twice), and every promotion writes agrant_promotedaudit record naming the actor, the path, the grant and the finding, because a grant that appears in config with no record of who promoted it is worse than a hand-written one, which at least has a git history. Two things it deliberately will not do: a trigger is not rehearsed, since its content arrives from whoever fires it and rehearsing an invented message would propose a grant for a command the real firing may never use; and an inventory write is reported as ungrantable, because a grant that could repoint a host would widen itself. Three notes from building it: the forced-preview collector is bound by turn id rather than carried in metadata, because the turn crosses the bus into another task and a collector on the wire would be a store the automation's own turn could reach; forced preview is asserted for every gated tool by enumerating them from the loader, since a test that only droveexecute_on_serverwould pass while a tool added later executed for real in a rehearsal; and two faults were caught by tests that already existed โ the pending-action file holdsasdict(job), so the new nested state came back as a plain dict and the store write then asked it forto_dict, and the gateway read a new agent method directly, which raisesAttributeErroron the partial stand-ins the startup tests pass and would have crashed a boot. Executor protocol is at version 4; both sides ship in one wheel, so a mismatch refuses rather than degrades. Documented on fourteen pages, includingwebsocket.mdon why these two routes needoperateand notapproveโ answering one suspended action isapprove, authorising all of them forever is strictly more. Verified: 8,720 Python tests, 1,165 WebUI tests, basedpyright clean in strict mode, ruff and eslint clean,tscclean, and a fulldocusaurus build. -
2026-08-21 ๐ ๏ธ Released v0.15.2 โ a mention could not create the automation it was for. "Report the uptime of
@server:barrahomeevery 5 minutes" โ the exact sentence the previous release added mentions for โ failed withTypeError: Object of type RuntimeContextBlock is not JSON serializable, and the failure did not stop at the call that made it. Three layers, found in that order: the WebUI attaches the turn's runtime context blocks to the inbound metadata and the cron tool copied that metadata verbatim into the job's origin, sojobs.jsoncould no longer be serialized โ origin metadata now goes throughpersistable_metadata, which drops what only describes the current turn, the blocks and the already-resolved mention payload whose names are stale the moment a resource is renamed, and the job keeps its own references to re-resolve instead.add_jobappended to the in-memory list before saving, so each failed attempt left a job the store never wrote, and the model retried twice: the WebUI listed two automations that no restart could recover and nojobs.jsoncontained. And every save serializes the whole store, so those two phantoms then failed every following tick โdream,heartbeatand the rest stopped persisting state until the gateway was killed, which is the layer that turns one bad job into an outage and the reason an unwritable job is now rolled back out of the store. The mention is not discarded, it is stored where it belongs: the resources the creating message referenced become the automation'sreferences, resolved again on each run โ what the editor's mention field already did, now also true of asking for the automation in chat, which is what made a mention in a chat message worth anything to an unattended run./triggercarried both bugs and gets both fixes. Subagents: the default of one was holding a hole shut. Each subagent built its ownFileStateswhile they all shared one workspace, and that tracker backs read-before-edit and read deduplication, so two running at once could not see each other's reads or edits and the guard that exists to catch a blind overwrite was looking the other way. One tracker per parent session now, shared by that session's subagents โ per session and not global on purpose, sinceFileStatesis deliberately session-scoped and its own docstring says read-dedup must not leak across sessions sharing the process. The field wasge=1with no upper bound, so a typo was a fork bomb against the provider account; it is 1โ8 in the schema and not only in the UI, with a test that the route and the schema agree on the range. Settings has an Agents section:agents.defaults.maxConcurrentSubagentswas config-only and is editable now, with its own nav entry rather than a row in Regional beside the timezone, which is where it first landed โ nothing about it is regional, and covering it made that section read as though the timezone were a subagent setting. The control carries the cost in its help text rather than in a doc page nobody opens while changing it: each subagent is a full conversation with the model. Alsowebsocket-clientis gone โ declared since the initial commit and imported by nothing, so every install resolved and downloaded it, checked three ways because the name is one character fromwebsockets, which the WebSocket channel and the gateway do use. Verified: 8,683 Python tests, 1,163 WebUI tests, basedpyright clean in strict mode, ruff and eslint clean,tscclean, and a fulldocusaurus build. -
2026-08-21 ๐ Released v0.15.1 โ reference a server or a diagram instead of searching for it. Typing
@in the composer now offersserver:anddiagram:beside the Apps and topics, narrowing as you type and matching the summary as well as the name, so@server:prodfinds a host tagged prod and not only one named that. The point is to skip a search: without a reference, a task that names a thing starts with the agent callinglist_serversand matching on a name โ cheap in a chat where you watch it pick and correct it, and neither cheap nor correctable in an automation, where the match is re-done on every unattended run, so a rename or a closer-matching host silently changes what it touches. The text carries the name and the sidecar carries the id, so a rename still resolves and the message stays readable; a name that cannot be one word โExample: blog on Azureโ falls back to the id. What the agent receives is a reference and a summary, never the record: aServerholdssecret_refand its reads belong to a capability gate, so the mention says this exists, it is called this, useget_serverand the gate decides, with tests asserting that no secret ref, no host and no username reaches the context block. A bare id and name would have been too little โ it removes the search and leaves the model guessing whether to fetch โ and both stores already publish the fields that answer that. Ids are validated against the store rather than trusted, and a token naming nothing stays plain text and sends nothing, so a typo cannot become a reference. Automations carry the reference, which is the half that matters:references: [{kind, id}]on cron jobs and triggers, resolved before the turn is built, so the model never sees a partially resolved context. A reference that stops resolving refuses the run terminally rather than through the retry budget โ a deleted server fails identically every attempt, and spending ten backed-off attempts re-learning that only delays the notification โ and a failure is delivered under every policy butnever, so it arrives even on an automation quiet for weeks. The editor takes the same mentions, so a reference is captured when authored and a broken one is shown while editing rather than at 03:00. A missing secrets key no longer reads as a deleted secret: found from a real approval that failed, where the gate did its job โ approve, then allow formutate.remoteandcredential.accessโ and the action failed with references secret 'y', which no longer exists while the secret sat untouched andNANOINFRA_SECRETS_KEYwas simply absent from that process.resolve_plaintextreturnsNonefor a missing secret and raises for an unset key; the executor was not catching the second, so it now names the variable, says the secret is untouched, and says where to set it. Four notes worth keeping from building it: the trigger regex had no colon in its class, so the menu closed the moment you typed one; the first attempt put the uuid in the text with a shortened chip, and since the composer draws mentions as an overlay behind a transparent textarea, the narrower decoration dragged the caret out of position, which is how it was reported; decoration and the wire payload were both keyed on click history, so a hand-pasted token got no chip and no reference on the wire; and those two were briefly two regexes disagreeing on a dotted name likebarrahome.org, one matching lazily and stopping at the first dot โ there is one shared helper now. Documented on every page a reader would arrive from, which took a second pass: the composer inwebui.md, the automation side inautomations.md, and โ added after the fact, because a reader on either dedicated page would not have learned the feature exists โsecrets-and-servers.mdbeside "Connect to<server name>and run uptime", anddiagrams.mdwith the distinction between the two verbs, since@diagram:references and/infradiagramsattaches. Two gaps closed with them:troubleshooting.mdhad nothing for approved, and it failed anyway, which is the symptom this release's diagnostic fix exists for, and the token scope model shipped in v0.15.0 was only mentioned in passing, sowebsocket.mdnow states the five scopes and the fail-closed default. Verified: 8,674 Python tests, 1,161 WebUI tests, basedpyright clean in strict mode, ruff and eslint clean,tscclean, and a fulldocusaurus build. -
2026-08-21 โฑ๏ธ Released v0.15.0 โ an automation you can leave running overnight. The surface was a name, a schedule and a block of prose, and a real job showed what that costs: its message carried a hand-picked state file at
$HOME/workspace/.cron_state/*.jsonfor dedup, a delivery policy phrased as "if nothing new, stay silent", an output contract, and a rawcurlagainst the GitHub API whileskills/github/sat there unmentioned. The message staying prose is the product โ the agent composes the steps rather than you wiring them โ but state and delivery are mechanism, and a prompt holds those worst. Both are fields now, and the rule that decided which: if the message could talk the platform out of it, it does not go in the message. So per-automation state is a tool the agent calls, scoped by the running turn rather than by an argument โ one automation cannot read or clear another's, and on a chat turn the tool refuses because there is no automation to scope to โ while delivery, retry and declared skills are enforced fields the message cannot reach. Delivery isalways/on-change/on-error/never, decided atTurnDelivery.completewhere a response reaches the bus, with a failure delivered under every policy butnever(a setting chosen to reduce noise was not chosen to hide a failure) and an empty success delivered under none.on-changecompares a whitespace-normalised fingerprint kept in a store theautomation_statetool has no method to reach, because an automation able to edit the record of what it last said could talk itself past the policy. Failure semantics were wrong in one subsystem and absent in the other: a retried trigger delivery was rewritten to the inbox and reclaimed on the next 0.5s poll, so ten attempts burned in about five seconds and dead-lettered before a briefly unavailable downstream could recover, while cron caught the error, recorded it and computed the next slot. One backoff helper now serves both โ exponential with full jitter, because the callers that matter are many gateways retrying one host โ and cron's optional policy got the three cases the shape had to reach: a skipped run consumes no attempt, a one-shot with attempts left is not switched off, and a retry displaces the next slot rather than sitting beside it. A trigger can finally be fired by something that only reaches a port.POST /api/triggers/{id}/fire, authenticated by that trigger's own key, of which only the SHA-256 is stored โ a deviation from the spec'ssecret://ref and the better shape, because a stolentriggers.jsonnow yields nothing and there is nothing to read back, which makes "issue again" the whole of rotation. The gateway's own tokens could not have served: they live in dicts keyed ontime.monotonic()and do not survive a restart. The handler does nothing butenqueue, so HTTP inherits the backoff, dead-letter, crash recovery and audit records the CLI path already had. The transport decided two things rather than me: the message travels in a header becauseprocess_requestexposes no body, and the 6000-byte cap sits underMAX_LINE_LENGTHbecause past that the connection closes with no status code at all โ my first attempt put it at 8KB, right at the cliff. A wrong key and a wrong id return an identical401, so the endpoint is not a trigger directory. A dead letter can now be replayed withreplay_oflineage, passing the same gates as the original, and a forced cron run is finally distinguishable in history from a scheduled one.api_tokenswas a baredict[str, float]andcheck_api_tokena boolean, so any API token was authority over every route โ there is one scope model now, defaulting tooperateso an unaudited route stays operator-only, with approvals and secrets declaring theirs. The token prefix stopped beingnbwt_. And an audit found four things "8636 passed" was hiding: the #80 guard was firing and the suite was green by import order, because a fixture replacedload_configand five webui modules kept the stand-in; nine warnings, now none; a skip that had outlived its reason and hid the fact that the executor's preview already names the resolved hosts an operator needs before approving; and five tests over an encrypted secret store that ran in no environment at all, which CI now runs against a real Postgres, with a step that fails if they skip because those tests skip when it is unreachable too. Verified: 8,637 Python tests, 1,147 WebUI tests, basedpyright clean in strict mode, ruff and eslint clean,tscclean, and a fulldocusaurus build. -
2026-08-19 โ๏ธ Released v0.14.0 โ seven channels are gone, and so is the text that still promised them.
dingtalk,feishu,mochat,napcat,qq,wecomandweixinare removed with their tests, manifests, locales and WebUI contributions; ten channels remain โ discord, email, matrix, mattermost, msteams, signal, slack, telegram, websocket, whatsapp โ and an existingconfig.jsonthat names a removed channel still loads, verified rather than assumed: the section survives as inert data, so an upgrade needs no edit. Optional dependencies went with the packages, because each channel declared its own in itsmanifest.pyrather than inpyproject.toml. Removing them orphaned four capabilities that existed for these channels alone, and each is now a test asserting the current truth instead of a test with no subject: multi-instance (feishu was the only one), interactive connectors (feishu and weixin), React UI contributions (only those two shipped awebui/index.tsxwith a Panel or ConnectFlow), and the channel UI alias table, which lived in their contributions โ an unclaimed alias now falls back to the name it was given. The contract machinery stays in every case, because it is what a future channel would declare against. Thezh-CNandzh-TWlocales go too: two centralcommon.jsonfiles, twenty per-channel ones, thesupportedLocalesentries, thezh-*browser-detection branches and both resource registrations, leaving eight locales. Six WebUI tests used those two as their exemplar locale with hardcoded Chinese strings; they now usejaandko, with the expected values read out of the surviving locale files rather than translated by hand โ which is how a real mismatch surfaced, sincesettings.overview.webSearchisWeb ๆค็ดขwhile the two sibling keys areใฆใงใๆค็ดข. Two tests the first removal had quietly broken are worth recording, both from its own name substitutions:test_no_channel_ships_an_interactive_connectorasserted onconnect, but the contract attribute isconnector(nanoinfra/channels/plugin.py:34), so the assertion was vacuous and would have passed with a channel shipping a connector; andtest_discover_plugins_excludes_internal_helpersclaimed_feishu_wsand_feishu_instancesstay out of discovery, except both modules were already gone, so it proved nothing. The same substitutions left a Discord test installing the Feishu SDK,qq-botpyin the not a required dependency list,_WeixinChannelnaming a Signal fake, and Feishu named as the example in two comments describing behaviour that is Slack's and Telegram's as well. Docs follow:channels.mdloses seven collapsible setup blocks โ 378 lines โ and the Feishu, WeChat and QQ guides are deleted with their sidebar entries and inbound links, whilechannel-package-guide.mdstops pointing atdingtalk/manifest.pyandfeishu/as its two worked examples, since both are 404s and no shipped channel is multi-instance, so that sentence promised an example a reader could not find. What stays is deliberate: the model, image, transcription and search providers, CJK as test data and the CJK-aware width and truncation handling โ deleting those removes wrapping and grapheme coverage, not a translation โ theja,ko,viandidlocales, and the brand-icon entries for third-party CLI apps in a remote catalog this project does not own, where removing an entry takes away a logo and not an app. 27,818 deletions against 295 insertions across 199 files. Verified: 8,471 Python tests, 1,140 WebUI tests, basedpyright clean in strict mode, ruff and eslint clean, and a fulldocusaurus buildโ this page throws on broken links. -
2026-08-18 ๐ Released v0.13.1 โ a saved diagram updates the WebUI live. Three writers reach
DiagramStorein-process โ the agent'supdate_diagramandcreate_diagramtools, the REST route, and a second browser tab โ and none of them told the browser anything, so a diagram written while the WebUI had it open stayed stale until a manual reload: a stale node count in the gallery, and an open canvas still showing a layout the agent had already replaced.nanoinfra/diagrams/changes.pyis the seam, and the store announces each write after it is durable; the registry is module-level because every caller builds its own store, so a listener bound to one instance would miss writes made through another, and a listener that raises is logged and skipped โ it runs after a durable write, and reporting an error there would make the caller retry a save that already landed. Nothing sensitive travels in the broadcast: the frame carries the id, kind and revision only and a client refetches the body over the authenticated REST route, so no node config and nosecret://reference leaves in a fan-out that goes to every connection โ which it must, because the Diagrams view is not attached to a chat. In the editor the decision comes from a content fingerprint rather than a dirty flag, so there is nothing to keep in step with a dozensetNodescallers: a clean canvas follows the server silently, unsaved edits get a banner, and the editor's own save is recognised as an echo instead of reported as someone else's change โ and the fingerprint normalises the defaultstoFlowNodes/toFlowEdgesfill in on load, without which every diagram saved beforetypeand the named handles existed would read as "changed on the server" the moment it opened. Deliberately in-process: a hand-editeddiagrams/<id>.jsonor agit checkoutnever calls the store and so is not announced, whichlist_diagramsalready reports asmodified_outsideon the next read. Also fixes the ruff import-order failure that broke the 3.14 job, and records the trap behind it โuv syncprunes the channel SDKs because they install from manifests rather thanuv.lock, and without thembasedpyrightreports 1441 errors from missing imports while the feishu optional-feature test fails, which reads exactly like a regression and is an empty environment. No breaking changes. Please see release notes for details. -
2026-08-18 ๐ Released v0.13.0 โ a security pass and an Agent Plugins v1 adoption, with every upstream fix verified against this tree before porting. Five holes were reproduced, not read about. Provider API keys reached CLI-app subprocesses:
_setup_envwrote the configured key into the agent's ownos.environandrunpassed an explicitos.environ.copy(), which is the path the agent uses to execute an installed app with its own arguments โ ExecTool was never affected, because its_build_envhas always allowlistedHOME/LANG/TERM. The ExecTool path guard could not see a path behind punctuation: its prefix class covered whitespace, pipe,>,=and quotes, socat </etc/shadowโ input redirection without a space, everyday shell โ extracted nothing at all and was never checked; the trailing-)case that looks like a bypass never was one, since/etc/shadow)still resolves outside the workspace and still blocks. A presigned URL went to a third party:web_fetchforwarded any URL tor.jina.ai,x-amz-*and userinfo included, and the fetcher holding no credential of its own is beside the point when the credential is in the URL. Session history lived inside the agent-owned workspace, so the account the privilege split exists to contain could rewrite its own transcripts; it moved out, keyed by a random id recorded in the workspace rather than a hash of its path, because hashing the path orphans every transcript the moment an operator renames a directory โ upstream shipped the hash first and had to replace it. And the API's per-session lock map grew without bound on a caller-suppliedsession_id; bounding it was the easy half, since a lock evicted while in use hands the next request a different lock and the two stop being serialized, so eviction is reference-counted and the bound yields to correctness when every lock is busy. Agent Plugins v1 arrives as an external multi-vendor standard rather than a format of our own โ verified: v1.0.0, TSC from Amazon, Cursor, Microsoft, OpenAI and Vercel. Skills load at workspace > plugin > builtin, plugin-declared stdio MCP servers launch in the confined mcp-host rather than wherever the caller lives, and activation authority istools.agentPluginsin a git-reviewed file: enabling a package that shipsmcp.jsongrants a new process, which is not a UI toggle's decision. The activation marker sits on the executor-owned side of the split, because upstream's location is sound for one account and agent-writable under ours. The Apps panel andnanoinfra agent-pluginsare read-only by necessity, not taste. Reviewing beat porting, measurably: of 36 upstream commits and PRs examined, eleven needed no work โ the Anthropic idle timeout, short-idle archiving and both delivery-retention fixes were already here โ and three would have damaged the tree: one is syntactically broken and de-indents a method out of its class, one would have deleted our subagent transcript redaction layer, and one inserts a mid-turnrole: "system"message which on Anthropic replaces the entire system prompt and makes the context governor discard real tool results, proven by one of our own tests failing under it. A fourth patches a method our runner never calls โ measured at 0 invocations across six tool rounds. Three faults of our own are worth recording. A strict-mode type error failed CI on every run for a day becausebasedpyrightwas run per-file instead of over the project, and over the project it needs the channel SDKs installed or 1441 errors from missing imports hide the real one. The same missing SDKs are why a feishu test was reported as "pre-existing" all day: with them installed the suite went from 8232 to 8823 passing โ roughly 600 tests that were never collected. And a gateway hook readconfig.agents.defaults.workspacedirectly, which raisedAttributeErroron the partial namespaces the startup tests pass and would have crashed the gateway. Also: cron survives a bad tick โ_arm_timersat outside thetry, and the tick runs in a task nobody awaits, so one exception ended the schedule in silence โ and never replays a job after a failed save. A timed-out command takes its process tree with it instead of orphaningcmd &grandchildren. The message bus is bounded without deadlocking the agent, which publishes inbound messages while being their only consumer. The prompt-token estimate is calibrated against what the provider actually charged, because no provider implements the counter the estimation chain prefers, so tiktoken was sizing Anthropic prompts against 1024 tokens of headroom. And canonical session files are serialized across processes: two writers each loading, appending and saving lost a turn, and the fix exposes the lock rather than only applying it, since serializing the individual operations leaves the mutation between them. BREAKING:tools.restrictToWorkspacenow defaults totrue; an existingconfig.jsonthat omits the key is pinned tofalseon load, so upgrading changes nothing and a startup warning says so. Please see release notes for details. -
2026-08-17 ๐ผ๏ธ Released v0.12.0 โ the workspace file viewer renders what it opens. It showed every file as syntax-highlighted source, so a diagram the agent had just written could only be read as flowchart text. A Raw / Preview toggle now appears only where it means something:
.mmdopens as a diagram,.svgas an image,.mdas source with prose one click away, and a.pycarries no toggle at all rather than a control that does nothing โ with the choice remembered per extension, under its own storage key because Settings holds the whole preferences object from mount and writes it back on any change. No backend work was required: those extensions are text and already arrived, and the only server change is that.mmdreports its language asmermaidrather thanmmd. A mermaid renderer was already in the tree, waiting โstreamdownships a full mermaid block with pan/zoom, fullscreen and download as mmd/svg/png, and renders a fence only when the app suppliesplugins.mermaid, so this release supplies that plugin instead of writing a second renderer. An.svgrenders as a data-URL<img>and never as markup, because a workspace file is untrusted input and secure static mode makes<script>,on*handlers and a remote@importinert by construction โ not hypothetical, since the SVG mermaid.ink returns embeds a CDN@import. A truncated file stays source with the toggle disabled: half an<svg>is not a smaller picture. Mermaid's own SVG had to be repaired before anything could consume it as an image, and two reports โ a PNG download that did nothing and a downloaded SVG that Edge refused withmismatched tag. Expected: </br>โ were one output with two defects, neither visible while it is injected into a page as HTML: labels come back with<br>unclosed, which is valid HTML and fatal XML, anduseMaxWidthgives the rootwidth="100%"with no height, so an<img>resolves to 0ร0 andtoBlobreturns an empty blob โ a failure that reports as success. The repair parses as XML and not HTML, because the obvious version that mermaid-cli uses rewrote the label<div>out of the XHTML namespace and into the SVG namespace, which parses cleanly and renders nothing: an exported diagram with every label missing. Two faults this work introduced are worth recording: a static import pulledstreamdowninto the eager graph andmanualChunksthen emittedsyntax-highlightandmarkdown-vendorimporting each other, whichtscandvite buildboth accepted while the app threwcan't access lexical declaration 'Q' before initializationon first paint โ now guarded by a test on the module graph, since a successful build cannot see it; and the repair logged the error it repairs, probing with a parse it expected to fail, which from the outside is indistinguishable from the defect. Also: an automation names the chat it is linked to โ a job from a chat titled "Weekly GitHub Issue Blocker Summary" showed/model deepseek, because a WebUI title lives atmetadata["title"]while the panel read the top level, so every automation linked to a WebUI chat resolved an empty title and fell through to a preview that skipped hidden records but not commands. And the composer says when an approval is waiting, with the/goalglow in amber โ sky means the agent is working, amber means it stopped and the next move is yours โ breathing slower because an approval can sit for minutes, displacing the sky glow when both apply, and firing for any pending approval rather than the open chat's, since one raised by a cron job is exactly the one you would miss. No breaking changes. Please see release notes for details. -
2026-08-17 ๐ Released v0.11.1 โ a dependency release: 24 Dependabot alerts, all
fixedrather than dismissed, so the vulnerable versions are gone from both lockfiles instead of being marked as accepted. Seven reached a browser, and the path is real rather than theoretical, because the WebUI renders model output and model output carries fetched web pages and shell results:mermaid(prototype pollution in the config APIs and in architecture diagrams, CSS injection reaching siblings, two DoS loops),dompurify(anIN_PLACEhook removal left a detached subtree executable), andprismjsโ that last one was hiding, because the top level already held the patched 1.30.0 whilerefractor/node_modules/prismjsheld 1.27.0 and that nested copy is what ships;refractor@3pins~1.27.0andreact-syntax-highlighter@15pinsrefractor@3, so it is only reachable through an override. The rest is test and build tooling that never ships:vite5 โ 6,vitest2 โ 3,happy-dom16 โ 20,esbuild,postcss,nanoid,@babel/core,brace-expansion. They are overrides and not dependencies, becausebun updateon a transitive package promotes it intodependenciesand takes the newest major with it. Two faults this work introduced are worth recording: the npm lockfile came out unusable because it was regenerated against a bun-creatednode_modulesโ 1 esbuild platform package instead of 26 โ andnpm ci --dry-run, which is exactly what theVerify npm lockfilestep runs, accepted it while the Docker image build caught it; and the pull-request auto-close workflow closed seven of Dependabot's own security PRs, because it carries noauthor_associationthat job accepts. A control that silences the repository's own vulnerability reporting is worse than the drive-by PRs it was written for. No product behaviour changed. Please see release notes for details. -
2026-08-17 ๐งฑ Released v0.11.0 โ a review of the memory, Dream and diagram subsystems produced 33 issues, and this release closes all of them, each with a test that fails first. Memory could be lost and now cannot:
write_filereachedmemory/history.jsonlwith no guard,compact_historyreplaced the file outside the append lock with a lock that was process-local while the CLI is a second process, five of six writers used a plainwrite_textso a torn.dream_cursorread as0โ which re-offered consolidated entries and pinned the compaction floor โ and a-5in that file disabled compaction permanently and silently. Two overlapping Dream runs both reported success and both advanced the cursor./dream-restorewrote every tracked file from the parent commit, so reverting one dream destroyed a later unrelated one while the listing named files it had not restored. Dream was shown the first 8 KB of a 16 KB MEMORY.md, told that was the file, told to prune, and handed a whole-file write. Dream now tells the truth about what it did: a run that mistyped one edit and then wrote the file correctly is no longer reported as failed and re-dreamt forever, a run whose only edit was a skill is committed and revertable rather than reported as "no memory changes" while its text reaches every later prompt, and conversation history arrives as data โ fenced and labelled, because it carries tool output. Diagrams stop losing writes:if dry_run:was the whole gate anddry_runis a parameter the model sets, so previewing one payload and applying another was accepted and a first-and-only call could empty a diagram; an apply now carries a digest of the preview that authorized it, recorded on the server and spent once, and every write reaches the audit log. Akind: "secret"field held a plaintext password behind the placeholder "stored in Secrets Manager" and now holds asecret://reference. An update re-packed the whole canvas because Python assumed a 300x130 node while the browser draws 220x90, so renaming one label moved 11 of 11 nodes with no undo. Two things about the tests: six shipped tests asserted the defect, and one suite could not fail โ the diagram route tests are titled end-to-end and ran through a double that builds the request in memory, so the gateway's 8192-byte header limit never applied and the double answered 200 for any size, which is exactly the bug it was covering. Breaking: a diagram save carrying a raw secret value is refused, an agent apply requires a matching preview, the diagram body travels in chunked headers,GitStore.revertreturns aRevertResult, anddream_run_completedtakesended_in_error. Please see release notes for details. -
2026-08-16 ๐งฉ Released v0.10.3 โ three fixes, each one found by using the thing. A repeated
tool_call_idno longer makes a session unusable:kimi-k3names a call after its slot, so callingexecin slot 3 on two turns producesexec_3twice, and three separate places treated that id as a name unique for the life of a session โ persistence dropped turn two's legitimate result as a duplicate, the request path would have dropped it again, and the repair that exists for exactly this shape could not see the orphan because turn one's result made the id look answered. A provider validates the pairing per assistant message and the refusal is not fallbackable, so the malformed transcript that feeds every later turn stayed broken for good: one collision bricked the session permanently. All three now scope the question to the assistant message being answered, and the guard each of them exists for keeps its own test. Upstream carries all three identically and has not fixed this; it fixed the sibling twice, and both deduplicate only within one response. An app installed from another virtualenv can be uninstalled: the state directory is shared across checkouts,uv pip uninstallexits 0 for a name it does not hold, and only the return code was read โ so a no-op read as a removal while the entry-point check kept the row forever. The row now goes and the file stays where the other environment put it, a no-op no longer claims a removal, and the panel readsavailable, because the gateway already answered this withstatus: "missing"and the row rendered a check mark frominstalledalone. Thread width is yours to choose: Settings โ Appearance offers Standard, Wide (new default) and Full, and prose and tables get different widths โ a line you cannot track back to its start is hard to read however much room exists, and a table that scrolls sideways is hard to read at any width, so the extra room goes to content that is not prose first. Please see release notes for details. -
2026-08-16 ๐ท๏ธ Released v0.10.2 โ a small one. The open sidebar shows the wordmark, which states the product name, where the square mark stated none. The rail width selects the asset and that is the whole rule: the open rail is 272 px and the wordmark takes 165 px of its 248 px of content, while the collapsed rail is 56 px and the wordmark asks for the same 165 px, so it misses by 109 px โ the square mark stays there for that reason and not for taste, and the favicon stays square because no aspect ratio makes a word legible at 16 px. The wordmark keeps its own colours, and the release reports the contrast rather than claiming it: 4.74:1 and 3.48:1 on the light sidebar, 2.31:1 and 3.15:1 on the dark one, where the darker strokes go dim. A logotype carries no WCAG minimum, so the numbers are stated and the asset is left alone. For whoever works on the code, a test run that strips its own environment now fails โ
uv sync --all-extras --devmatches the lock and therefore removes every channel SDK, because those install outside the lock from each channel manifest, and two states were then invisible while they were wrong:basedpyrightreports 0 errors with them and 1425 without, and the suite collects 8366 passed with them and 8009 without, still printing green. The guard compares installed distributions in both directions, because a test that installs a package is as wrong as one that removes a package, and its failure message carries the one-line recovery. Please see release notes for details. -
2026-08-16 ๐ Released v0.10.1 โ v0.10.0 made an approver a person; this makes it visible, because a control nobody can see is a control nobody checks. Settings โ Overview carries a Security section: who you are, what an agent may do to a host in plain language, and whether anything waits for a person. That phrase is derived from the weakest decision in the policy and never from an average, so it cannot understate what a deployment allows, and it names how many standing grants exist rather than claim a person answers each action โ a grant is exactly what bypasses an approval. Settings โ Gates opens with the kind of authentication that is installed: a shared token, a verified assertion with its issuer and claim, a
plainassertion that is read and never verified, orallowAnyVerifiedIdentity. It warns when a configured proxy asserted nobody, a state that left every approval naming nobody while the deployment believed it named somebody. The gateway sends a posture kind and never a sentence, because ten locales cannot translate one, and it sends no secret, no key material and no address list. Fixes: the gate panel threw on a payload missing a cosmetic field instead of rendering without it, and WhatsApp could not link a device because theneonizepin excluded the line carrying whatsmeow's protocol fixes โ the floor moved with the ceiling, so an existing install receives it too. The WhatsApp guide also says the two things that cost a reader an attempt each: message the bot from a different account, because the account you linked is the bot, and approve the code the bot sent rather than the example on the page. Please see release notes for details. -
2026-08-16 ๐ค Released v0.10.0 โ an approver can be a person. Until now the WebUI authenticated a token and never a person, so an audit record said "the WebUI approved it" and never who did. A proxy in front authenticates the person, and nanoinfra verifies the assertion rather than trusting it: RS256 against the provider's key set, with the issuer, the audience and the claim from git-reviewed config.
gates.approversthen nameswebui:<claim>, and every audit record that holds an actor holds a person โ including who asked, not only who answered. The identity provider is interchangeable, because nanoinfra never talks to it except to fetch that key set: Keycloak, Google, Azure AD, Authentik, Dex and Cloudflare Access are three values of config.examples/auth/runs the whole chain with Dex and oauth2-proxy, and a new guide walks it. Two lists, two jobs: the proxy allowlist andallowedIdentitiesdecide who reaches the agent, andgates.approversonly decides whose approval counts โ a verified stranger is still a stranger, so ajwtblock that names nobody refuses to load.gates.identityIndependencelets a second person be a second factor, off by default, and the release notes say what that trades. Breaking:assertionFormatdefaults tojwt, so a two-fieldtrustedProxyAuthblock fails at startup. Please see release notes for details. -
2026-08-15 ๐งน Released v0.9.2 โ one session file, five writers, and now five scrubs. v0.9.1 moved the credential scrub into the executor and covered the message records. It turned out four other paths wrote into the same
sessions/*.jsonl: the reasoning of a turn, the runtime checkpoint the agent saves on every turn that runs a tool, the chat-style messages of a provider's replay cache, and the provider's own replay payload on the Responses and Codex paths. Each one was found by looking for the next writer after closing the previous one. An approval can now be answered from a chat channel with/approveand/deny, so a deployment that runs only the WebUI stops having a second path in name alone. The audit log records what happened, not only what was decided: a secondcompletionrecord carries the exit code and the duration, and it says "unknown" for a timeout rather than guessing zero. Also: a stdio MCP child no longer outlives a host killed withSIGKILL, a restored tool result is bounded like every other record, and a swallowed circular import no longer leaves the rootConfigclass unusable for the life of a process. One visible behaviour change: a turn whose reasoning held a stored secret replays with no reasoning block, because a provider signs that text and the scrub changes it. Please see release notes for details. -
2026-08-15 ๐ฉน Released v0.9.1 โ a patch release for what v0.9.0 shipped broken. The latch clear and the approvals inbox could not answer, because both writes were a POST and the WebSocket transport serves GET alone. An allowed action refused its own credential, so
mutate.remote: allowwith the shippedcredential.access: approverefused every remote action against a server that holds asecretRef. The Secrets form could not take an SSH private key, because a single-line input collapsed the paste. A diagnosed traceback could write a plaintext credential into a log file, and the agent process no longer decrypts every secret to build the redaction sentinels: that scrub runs in the executor now. Please see release notes for details. -
2026-08-15 ๐ Released v0.9.0 โ The Short Leash Release makes the security posture on the landing page true in the code: every mutating and remote-execution tool now passes a capability gate keyed on a capability class and a resolved scope of one host, a host group, or all hosts, with policy in a new
gatesblock and unattended remote execution denied by default. The agent asks, and another process decides: remote execution, web access, and stdio MCP servers each run in their own process behind a Unix socket, under their own account, and under a Landlock policy, so the agent holds neither the credential store nor a transport. An unusual action waits for a human on a different path than the request arrived on, over the exact command and host list the executor rendered, and recurring work uses standing grants instead. A denial is terminal, latches the class for that session, and survives a restart because an append-only audit log rebuilds it. Four new WebUI surfaces: the approvals inbox with an unread count, the gate policy panel, the latch banner, and the audit log viewer. Please see release notes for details. -
2026-08-13 ๐ก Released v0.7.0 โ The Homestead Release moves nanoinfra onto infrastructure it owns and narrows what it claims to be: the project lives under the
nanoinfraorgorganisation, the documentation has its own site at docs.nanoinfra.org, and skills come from a self-hosted catalog at skills.nanoinfra.org rather than a third-party registry. nanoinfra is now positioned as an agent for infrastructure work โ inventory, credentials, remote execution, topology diagrams โ not a general-purpose assistant. Windows support is dropped: the PowerShell installer and all Windows instructions are gone, and supported platforms are Linux and macOS.skillsMarketplaceis also accepted as a camelCase config key, which it previously rejected. Please see release notes for details. -
2026-08-07 ๐ Released v0.6.0 โ The Continuity Release adds durable subagent transcripts (every background subagent run leaves a full conversation transcript under
memory/subagents/), drag-to-attach sessions in the composer with persistent manual sidebar ordering, trusted-proxy bootstrap auth for the WebUI, two real session-retention/Dream data-loss fixes (dropped proactive deliveries during trimming, short idle sessions never archived), Mattermost per-thread group policy, hardened WhatsApp media handling, and per-request hosted web/X search toggles. Please see release notes for details. -
2026-08-07 ๐ Released v0.5.0 โ The Keyring Release adds encrypted Secrets storage (local or Postgres-backed, write-only โ no tool or API ever returns a decrypted value) and a Server inventory the agent can connect to and run real commands/actions on via SSH, Ansible Runner, AWS SSM, or a configured HTTP API, with durable job records, idle-aware timeouts, and a network guard against metadata/loopback targets. Both modules get a full WebUI management page under a new "Infrastructure" sidebar section, and the Diagrams target picker now lists real inventoried servers. Please see release notes for details.
-
2026-08-04 ๐ Released v0.4.0 โ The Blueprint Release adds Infra Diagrams: a visual designer for the infrastructure you run, backed by real workspace persistence, a dynamic component catalog, the
/infradiagramscommand, and agent tools that can propose and apply diagram changes with an explicit approval gate. Please see release notes for details. -
2026-07-24 ๐งญ Guided first-run setup, inline subagents, and model switching from the composer.
-
2026-07-23 ๐ Grok OAuth with hosted X Search, live image settings, and clearer fallback models.
-
2026-07-22 ๐ Parallel Search, live configuration reloads, richer app discovery, and a smoother mobile WebUI.
-
2026-07-21 โก Codex fast mode, visible skill references, safer configuration saves, and sturdier task cleanup.
-
2026-07-20 ๐ฌ Cleaner code blocks and copy actions, self-contained channels, and steadier QQ reconnects.
-
2026-07-19 ๐ Cross-provider failover, safer local triggers, WhatsApp group allowlists, and sturdier workspace staging.
-
2026-07-18 ๐งฐ More resilient automation recovery and UTF-8 CLI App installs.
-
2026-07-17 ๐ Kimi K3 support, more reliable scheduled jobs, and cleaner provider behavior.
-
2026-07-16 ๐ Native folder picker bridges, tighter Docker defaults, and bounded session caching.
-
2026-07-15 ๐ Short-lived Render access, safer gateway shutdown, validated file previews, and highlighted app mentions.
-
2026-07-14 ๐ Document attachments, one-click Render deployment, clearer workflow docs, and stronger Windows support.
-
2026-07-13 ๐ Guided WebUI setup, Brazilian Portuguese, and steadier Dream, gateway, and Discord behavior.
-
2026-07-12 ๐ฏ Explicit
/goalactivation, safer runtime and workspace access. -
2026-07-11 ๐ ๏ธ Syntax-highlighted previews and diffs, queued prompts, safer edits.
-
2026-07-10 ๐ง Stable model routing, multiline CLI input, new automation guide.
-
2026-07-09 ๐ Live file-edit diffs, safer localhost setup, Matrix image fixes.
-
2026-07-08 ๐ Safer WebUI/API setup, onboard refresh, responsive prompt rail.
-
2026-07-07 โจ๏ธ CLI multiline input, steadier slash commands, safer web fetching.
-
2026-07-06 ๐ฌ Mattermost channel, Serper search, safer Windows shells.
-
2026-07-04 ๐ MCP reconnects, safer Copilot refresh, Windows shutdown fixes.
-
2026-07-03 ๐ง Guided WebUI setup, plugin controls, Claude Sonnet 4.6 default.
-
2026-07-02 โฐ Local triggers with recovery, audit history, WebUI pending status.
-
2026-07-01 ๐ก๏ธ API keys for remote binds,
$skillshortcuts, clearer tool errors. -
2026-06-30 ๐ Provider proxies, Copilot Enterprise, steadier WhatsApp and Weixin.
-
2026-06-29 ๐ง Context replay scaled to model windows, without fixed message caps.
-
2026-06-28 ๐ผ๏ธ MCP images, steadier WebUI reconnects, safer tool calls.
-
2026-06-27 ๐ Collision-safe sessions, safer shells, Neonize WhatsApp.
-
2026-06-25 ๐๏ธ Thinking controls, MiMo voice input, opt-in Telegram rich messages.
-
2026-06-24 ๐ Kimi Coding and OpenCode, steadier reasoning and Anthropic tool calls.
-
2026-06-22 ๐ Released v0.2.2 โ The Durability Release makes nanoinfra sturdier for daily agent work: segmented WebUI transcripts, first-class Python SDK runtime controls, automation management, richer search/STT providers, and stronger gateway/session/provider reliability. Please see release notes for details.
-
2026-06-21 ๐งฐ Python SDK runtime controls, optional Keenable key, cleaner run hooks.
-
2026-06-20 ๐ฌ Telegram rich messages, safer SDK concurrency, smoother Quick Start.
-
2026-06-19 ๐ Firecrawl app, OpenAI image edits, safer session deletion.
-
2026-06-18 ๐ฌ Feishu recovery, Keenable search, Mistral polish, workspace-aware git.
-
2026-06-17 ๐ง Default idle auto-compact, clearer
/dream, macOS installer fixes. -
2026-06-16 ๐ฏ Fresher goal context, Kimi K2.7 thinking, cleaner API retries.
-
2026-06-15 ๐ฑ Mobile WebUI polish, optional file tools, real API usage.
-
2026-06-14 ๐ผ๏ธ Themed cover, partner links, stronger Codex image streaming.
-
2026-06-13 ๐๏ธ Session-bound automations, sturdier WhatsApp, faster WebUI startup.
-
2026-06-12 ๐ฌ Slack allowlisted channels can require mentions.
-
2026-06-11 โ๏ธ Fenced-code message splitting.
-
2026-06-10 ๐ Segmented transcripts, Exa/Bocha search, StepFun/SiliconFlow ASR.
-
2026-06-09 ๐๏ธ Shared voice input, more STT providers, TeX and email polish.
-
2026-06-08 ๐งฎ Token heatmap fix, safer MCP HTTP probing, docs cleanup.
-
2026-06-06 ๐งฐ SDK MCP cleanup, removable OpenAI image defaults.
-
2026-06-05 ๐ผ๏ธ Azure AAD, custom image providers,
/skill, steadier pairing. -
2026-06-04 ๐ MCP reconnects,
uv pipinstall fallback, QQ pairing. -
2026-06-03 ๐ง Hidden-history recovery, quieter email progress handling.
-
2026-06-02 ๐ฌ Email attachments, Napcat QQ, Volcengine search, simpler Dream.
-
2026-06-01 ๐ Released v0.2.1 โ The Workbench Release turns the packaged WebUI into a daily agent workbench: clearer Thought/response timelines, live file-edit activity, project workspaces, model and context controls, steadier sustained goals, CLI Apps + MCP extensions, and broader provider/channel support. Please see release notes for details.
-
2026-05-30 ๐ Safer Matrix verification, bounded media downloads, clearer WebUI model timeline.
-
2026-05-29 ๐งฉ Extension registry, context-window tuning, document extraction controls.
-
2026-05-28 ๐๏ธ Project workspaces, access controls, steadier goals and streaming.
-
2026-05-27 โฑ๏ธ Codex streams respect idle timeouts during long runs.
-
2026-05-26 ๐ก Telegram webhooks, refreshed Kagi search, cleaner transport errors.
-
2026-05-25 ๐ Unified CLI Apps and MCP, Step Plan support, steadier sustained goals.
-
2026-05-24 ๐งฐ MCP presets, richer slash actions, configurable OpenAI-compatible requests.
-
2026-05-23 ๐ผ๏ธ Zhipu image generation, longer exec windows, cleaner transcription config.
-
2026-05-22 ๐ ๏ธ CLI Apps, more image providers, safer web redirects and edits.
-
2026-05-21 โก Novita provider, faster sidebar, smoother coding tools and Weixin replies.
-
2026-05-20 ๐ถ Signal channel, faster gateway startup, multilingual README links.
-
2026-05-19 ๐จ Image provider registry, StepFun and Skywork, stronger WebUI controls.
-
2026-05-18 ๐๏ธ Gemini and MiniMax images, Ant Ling, live file-edit activity.
-
2026-05-17 ๐ Smoother WebUI streaming, AutoCompact fixes, buffered CLI reasoning.
-
2026-05-16 ๐ง Atomic Chat provider, goal-aware timeouts, safer exec URL handling.
-
2026-05-15 ๐ Released v0.2.0 โ
/goalholds sustained objectives across turns, WebUI now ships inside the wheel, image generation end to end, 5 new providers withfallback_models, and a real agent-loop refactor. Please see release notes for details. -
2026-05-14 ๐ฏ
/goalfor long-term objectives, visible multi-step progress, long-horizon missions in chat. -
2026-05-13 ๐ง Streaming reasoning before answers, automatic backup models, smoother plug-in reconnects.
-
2026-05-12 ๐๏ธ Saved model presets with WebUI badge, simpler plug-in tools, quieter Feishu topic threads.
-
2026-05-11 ๐ฅ๏ธ NVIDIA NIM support, terminal bot name and icon, streamed reasoning and MiMo toggle clarity.
-
2026-05-09 ๐ผ๏ธ Sharper image replay, BYO web-search keys in Settings, Feishu threads routed cleanly.
-
2026-05-08 โจ Inline chat image, redesigned Settings and keys, Dream memory aligned with visible history.
-
2026-05-07 ๐ Locale-aware slash palette in WebUI, LAN login, faithful HTTP streaming responses.
-
2026-05-06 ๐งฉ Tunable tool hint, steadier voice and plug-in startups, schedules and reminders that stick.
-
2026-05-05 ๐ก๏ธ Quiet deny for unknown Telegram chats, Dream cleanup, fuller automation summaries.
-
2026-05-04 ๐ Safer DingTalk outbound media links, durable cron persistence, DeepSeek polish.
-
2026-05-03 โ๏ธ Predictable shell allow-list behavior, isolated chats mid-reply, cleaner interactive retries.
-
2026-05-02 ๐ LongCat support, smarter token sizing hints, clearer bundled upgrade guidance.
-
2026-05-01 โ๏ธ Native AWS Bedrock provider, tighter helper handoffs and scoped session files.
-
2026-04-30 ๐ฌ Feishu threads that honor replies and topics, WhatsApp bridge refresh on source edits.
-
2026-04-29 ๐ Released v0.1.5.post3 โ Smarter threads on Feishu, Discord, Slack, and Teams; DeepSeek-V4; Hugging Face & Olostep; choices,
/history, and steadier long chats. Please see release notes for details. -
2026-04-28 ๐ Olostep web search, Hugging Face provider, safer workspace-tool interruptions.
-
2026-04-27 ๐ฌ
/historycommand, smarter session replay caps, smoother Discord / Slack threads. -
2026-04-26 ๐งญ Natural cron reminders, thread-aware restarts, safer local provider and shell behavior.
-
2026-04-25 ๐งฉ
ask_userchoices, macOS LaunchAgent deployment, MSTeams stale-reference cleanup. -
2026-04-24 ๐ฅ Video attachments for channels, DeepSeek thinking control, faster document startup.
-
2026-04-23 ๐งต Discord thread sessions, Telegram inline buttons, structured tool progress updates.
-
2026-04-22 ๐ GitHub Copilot GPT-5 / o-series support, configurable web fetch, WebUI image uploads.
-
2026-04-21 ๐ Released v0.1.5.post2 โ Windows & Python 3.14 support, Office document reading, SSE streaming for the OpenAI-compatible API, and stronger reliability across sessions, memory, and channels. Please see release notes for details.
-
2026-04-20 ๐จ Kimi K2.6 support, Telegram long-message split, WebUI typography & dark-mode polish.
-
2026-04-19 ๐ WebUI i18n locale switcher, atomic session writes with auto-repair.
-
2026-04-18 ๐งช Initial WebUI chat, smarter setup wizard menus, WebSocket multi-chat multiplexing.
-
2026-04-17 ๐ช Windows & Python 3.14 CI, Dream line-age memory, email self-loop guard.
-
2026-04-16 ๐ก SSE streaming for OpenAI-compatible API, Discord channel allow-list.
-
2026-04-15 ๐๏ธ LM Studio & nullable API keys, MiniMax thinking endpoint, runtime SelfTool.
-
2026-04-14 ๐ Released v0.1.5.post1 โ Dream skill discovery, mid-turn follow-up injection, WebSocket channel, and deeper channel integrations. Please see release notes for details.
-
2026-04-13 ๐ก๏ธ Agent turn hardened โ user messages persisted early, auto-compact skips active tasks.
-
2026-04-12 ๐ Lark global domain support, Dream learns discovered skills, shell sandbox tightened.
-
2026-04-11 โก Context compact shrinks sessions on the fly; Kagi web search; QQ & WeCom full media.
-
2026-04-10 ๐ Multiple MCP servers, Feishu streaming & done-emoji.
-
2026-04-09 ๐ WebSocket channel, unified cross-channel session,
disabled_skillsconfig. -
2026-04-08 ๐ค API file uploads, OpenAI reasoning auto-routing with Responses fallback.
-
2026-04-07 ๐ง Anthropic adaptive thinking, MCP resources & prompts exposed as tools.
-
2026-04-06 ๐ฐ๏ธ Langfuse observability, unified Whisper transcription, email attachments.
-
2026-04-05 ๐ Released v0.1.5 โ sturdier long-running tasks, Dream two-stage memory, production-ready sandboxing and programming Agent SDK. Please see release notes for details.
-
2026-04-04 ๐ Jinja2 response templates, Dream memory hardened, smarter retry handling.
-
2026-04-03 ๐ง Xiaomi MiMo provider, chain-of-thought reasoning visible, Telegram UX polish.
-
2026-04-02 ๐งฑ Long-running tasks run more reliably โ core runtime hardening.
-
2026-04-01 ๐ GitHub Copilot auth restored; stricter workspace paths; OpenRouter Claude caching fix.
-
2026-03-31 ๐ฐ๏ธ WeChat multimodal alignment, Discord/Matrix polish, Python SDK facade, MCP and tool fixes.
-
2026-03-30 ๐งฉ OpenAI-compatible API tightened; composable agent lifecycle hooks.
-
2026-03-29 ๐ฌ WeChat voice, typing, QR/media resilience; fixed-session OpenAI-compatible API.
-
2026-03-28 ๐ Provider docs refresh; skill template wording fix.
-
2026-03-27 ๐ Released v0.1.4.post6 โ architecture decoupling, litellm removal, end-to-end streaming, WeChat channel, and a security fix. Please see release notes for details.
-
2026-03-26 ๐๏ธ Agent runner extracted and lifecycle hooks unified; stream delta coalescing at boundaries.
-
2026-03-25 ๐ StepFun provider, configurable timezone, Gemini thought signatures.
-
2026-03-24 ๐ง WeChat compatibility, Feishu CardKit streaming, test suite restructured.
-
2026-03-23 ๐ง Command routing refactored for plugins, WhatsApp/WeChat media, unified channel login CLI.
-
2026-03-22 โก End-to-end streaming, WeChat channel, Anthropic cache optimization,
/statuscommand. -
2026-03-21 ๐ Replace
litellmwith nativeopenai+anthropicSDKs. Please see commit. -
2026-03-20 ๐ง Interactive setup wizard โ pick your provider, model autocomplete, and you're good to go.
-
2026-03-19 ๐ฌ Telegram gets more resilient under load; Feishu now renders code blocks properly.
-
2026-03-18 ๐ท Telegram can now send media via URL. Cron schedules show human-readable details.
-
2026-03-17 โจ Feishu formatting glow-up, Slack reacts when done, custom endpoints support extra headers, and image handling is more reliable.
-
2026-03-16 ๐ Released v0.1.4.post5 โ a refinement-focused release with stronger reliability and channel support, and a more dependable day-to-day experience. Please see release notes for details.
-
2026-03-15 ๐งฉ DingTalk rich media, smarter built-in skills, and cleaner model compatibility.
-
2026-03-14 ๐ฌ Channel plugins, Feishu replies, and steadier MCP, QQ, and media handling.
-
2026-03-13 ๐ Multi-provider web search, LangSmith, and broader reliability improvements.
-
2026-03-12 ๐ VolcEngine support, Telegram reply context,
/restart, and sturdier memory. -
2026-03-11 ๐ WeCom, Ollama, cleaner discovery, and safer tool behavior.
-
2026-03-10 ๐ง Token-based memory, shared retries, and cleaner gateway and Telegram behavior.
-
2026-03-09 ๐ฌ Slack thread polish and better Feishu audio compatibility.
-
2026-03-08 ๐ Released v0.1.4.post4 โ a reliability-packed release with safer defaults, better multi-instance support, sturdier MCP, and major channel and provider improvements. Please see release notes for details.
-
2026-03-07 ๐ Azure OpenAI provider, WhatsApp media, QQ group chats, and more Telegram/Feishu polish.
-
2026-03-06 ๐ช Lighter providers, smarter media handling, and sturdier memory and CLI compatibility.
-
2026-03-05 โก๏ธ Telegram draft streaming, MCP SSE support, and broader channel reliability fixes.
-
2026-03-04 ๐ ๏ธ Dependency cleanup, safer file reads, and another round of test and Cron fixes.
-
2026-03-03 ๐ง Cleaner user-message merging, safer multimodal saves, and stronger Cron guards.
-
2026-03-02 ๐ก๏ธ Safer default access control, sturdier Cron reloads, and cleaner Matrix media handling.
-
2026-03-01 ๐ Web proxy support, smarter Cron reminders, and Feishu rich-text parsing improvements.
-
2026-02-28 ๐ Released v0.1.4.post3 โ cleaner context, hardened session history, and smarter agent. Please see release notes for details.
-
2026-02-27 ๐ง Experimental thinking mode support, DingTalk media messages, Feishu and QQ channel fixes.
-
2026-02-26 ๐ก๏ธ Session poisoning fix, WhatsApp dedup, Windows path guard, Mistral compatibility.
-
2026-02-25 ๐งน New Matrix channel, cleaner session context, auto workspace template sync.
-
2026-02-24 ๐ Released v0.1.4.post2 โ a reliability-focused release with a redesigned heartbeat, prompt cache optimization, and hardened provider & channel stability. See release notes for details.
-
2026-02-23 ๐ง Virtual tool-call heartbeat, prompt cache optimization, Slack mrkdwn fixes.
-
2026-02-22 ๐ก๏ธ Slack thread isolation, Discord typing fix, agent reliability improvements.
-
2026-02-21 ๐ Released v0.1.4.post1 โ new providers, media support across channels, and major stability improvements. See release notes for details.
-
2026-02-20 ๐ฆ Feishu now receives multimodal files from users. More reliable memory under the hood.
-
2026-02-19 โจ Slack now sends files, Discord splits long messages, and subagents work in CLI mode.
-
2026-02-18 โก๏ธ nanoinfra now supports VolcEngine, MCP custom auth headers, and Anthropic prompt caching.
-
2026-02-17 ๐ Released v0.1.4 โ MCP support, progress streaming, new providers, and multiple channel improvements. Please see release notes for details.
-
2026-02-16 ๐ฆ nanoinfra now integrates a ClawHub skill โ search and install public agent skills.
-
2026-02-15 ๐ nanoinfra now supports OpenAI Codex provider with OAuth login support.
-
2026-02-14 ๐ nanoinfra now supports MCP! See MCP section for details.
-
2026-02-13 ๐ Released v0.1.3.post7 โ includes security hardening and multiple improvements. Please upgrade to the latest version to address security issues. See release notes for more details.
-
2026-02-12 ๐ง Redesigned memory system โ Less code, more reliable. Join the discussion about it!
-
2026-02-11 โจ Enhanced CLI experience and added MiniMax support!
-
2026-02-10 ๐ Released v0.1.3.post6 with improvements! Check the updates notes and our roadmap.
-
2026-02-09 ๐ฌ Added Slack, Email, and QQ support โ nanoinfra now supports multiple chat platforms!
-
2026-02-08 ๐ง Refactored Providersโadding a new LLM provider now takes just 2 simple steps! Check here.
-
2026-02-07 ๐ Released v0.1.3.post5 with Qwen support & several key improvements! Check here for details.
-
2026-02-06 โจ Added Moonshot/Kimi provider, Discord integration, and enhanced security hardening!
-
2026-02-05 โจ Added Feishu channel, DeepSeek provider, and enhanced scheduled tasks support!
-
2026-02-04 ๐ Released v0.1.3.post4 with multi-provider & Docker support! Check here for details.
-
2026-02-03 โก Integrated vLLM for local LLM support and improved natural language task scheduling!
-
2026-02-02 ๐ nanoinfra officially launched! Welcome to try ๐ nanoinfra!