Repository navigation
feat(agents): limit saved agents to one folder - #13009
Merged
Merged
Conversation
A saved agent gets FolderID, the workspace node of its folder, and FolderMode, read or write. The folder is kept by node so that a rename or a move is followed. A mode without a node is an agent whose folder is not chosen yet; Validate accepts it, and refuses a node without a mode. The GORM store adds two columns with defaults, so AutoMigrate fills the rows of existing agents as agents without a folder. The JSON store reads a file without the keys the same way. Both stores pass the new contract case. Assisted-by: Claude:claude-opus-5-5
A chat whose agent is deleted runs today without the agent's grants. When that agent had a folder, nothing is left to say which folder the chat may read, so the chat must be refused instead of getting the whole home. ChatRecord.ScopedAgent records that the agent had a folder. CreateChat writes the flag and SetScopedAgent changes it. UpdateChat leaves it alone, like the tool switches, so the loop's stale copy of the record cannot clear it. Both chat stores read chats stored before the flag as not scoped; the GORM column has a default, so AutoMigrate is the migration. Assisted-by: Claude:claude-opus-5-5
A saved agent works on the whole home folder of its user. The spec lets the user give an agent one folder and a mode, read only or read and write, and confines every chat that runs it to that folder. The workspace store enforces the folder through a fence on the actor, so a caller that forgets it gets less access, not more. The spec maps every caller that builds a user actor today, including the host bash, the sandbox runner and the add-on workspace operations, and how each is moved to the chat's access. The plan splits the work into ten lanes; the store fence and the agent record come first. Assisted-by: Claude:claude-opus-5-5
A person chooses an agent's folder by its path in their home; the
agent keeps the node, so a rename or a move is followed. The new
FolderResolver turns the path into the node and back, always as the
user. A path is refused when it is not a live folder, when it is
LocalAI/ or Chats/ or inside them, or when it names a parent. The
home itself ("/") is allowed.
The workspace stores cannot find a node by its ID yet. PathOf reads
the newest change sets of the home first, checks the path it finds
with Stat, and else lists the folders the user can read. A store that
implements NodeByID is used instead. Nothing is cached, so every
frontend answers the same.
SaveAgent keeps the stored folder when it replaces an agent, so a
caller that knows nothing of folders cannot widen one.
SaveAgentWithFolder sets or clears it. ImportAgent looks the folder of
a file up in the importer's home, and saves a folder that is not there
as unset: the mode without a node, which does not start.
Assisted-by: Claude:claude-opus-5-5
A chat whose saved agent has a folder must reach only that folder and the chat's own folders. The store needs to know which folders those are on every call, so the actor carries them. Actor.Fenced returns a copy that reaches only the given folders, by node ID, each at most at its mode. The fence is an unexported field: a copy of the actor keeps it, and only Fenced can set it. Fencing a fenced actor adds a layer, and a node is reached only when every layer allows it, so a fence never grows. The helpers in export.go let both store implementations apply the same rules: the mode a fence allows on a node, its roots, and the limit of MaxFenceRoots roots per call. Assisted-by: Claude:claude-opus-5-5
The agent routes carry folder. A write takes {path, mode}: the server
turns the path into the folder's node, and refuses with 400 a path
that is not a folder of the user's home, or is LocalAI or Chats. A
write without folder keeps the agent's folder, so a client that does
not know the field cannot widen the agent; {path: ""} clears it. A
read answers the folder's path now, its name, the mode and a state:
ok, missing or unset. The node never leaves the server.
An agent with a folder exports as version 2, with the folder's path
and mode. A server that reads only version 1 refuses that file, so a
scoped agent is never imported elsewhere as one with all the files.
An agent without a folder still exports as version 1. The import
reads both versions and looks the folder up in the importer's home;
a folder that is not there saves the agent as unset, with a warning.
The Swagger spec, the API instructions and the agents docs page show
the field and the new file version.
Assisted-by: Claude:claude-opus-5-5
The store is the one place every file call of a chat goes through, so it applies the fence: a caller that forgets the agent's folder gets "not found", never the whole workspace. - The mode of a fenced actor on a node is the lower of its grants and the fence. A root in the Trash counts for nothing, and is back after Undo, because the fence holds the node ID. - A fenced actor never takes the administrator shortcut, so it cannot grant, revoke or list grants, and ReadablePrefixes gives only fence roots or folders inside them. - A root cannot be deleted, moved or renamed by the fenced actor, by any path: Move, Delete, Undo, an upper layer, or a delete of a folder above a nested root. - A fence with too many roots or a bad root is ErrInvalid. The shared suite in wstest runs the new specs against the JSON store and the GORM store (SQLite and PostgreSQL). Assisted-by: Claude:claude-opus-5-5
Assisted-by: nib:gpt-6.1-sol
A chat's file tools spelled every path as a path in the user's home. A chat whose agent has a folder sees that folder at /workspace/home and its own chat folder at /workspace/chat, so the mapping must come from the mounts of the chat's view, not from the principal. session.Place maps a path the model writes to a workspace path: "/x", "x", "home/x" and "/workspace/home/x" are x in the primary mount, "/workspace/<name>/x" is x in the mount of that exact name, and the skill layers keep their names. It refuses "..", NUL, control characters and invalid UTF-8 wherever they point, and a /workspace path that is no mount. Encoded and look-alike separators stay part of a name. Place is only a mapping: the store's fence enforces the access. session.Access holds a turn's actor, view and place. It has no exported fields and one constructor, so a tool cannot be built from a principal string. Owner gives the user's own actor without the fence. Assisted-by: Claude:claude-opus-5-5
BuildView took the home of the principal as the folder at /workspace/home. A chat whose agent has a folder needs that folder there, at the agent's mode, and its own chat folder beside it. BuildView now takes the primary folder and the chat folder. The chat folder is mounted at /workspace/chat, read and write; the name chat is reserved like the skill layer names. The store caps each mount at the actor's mode, and a fenced actor gets the shared folders read only. Every caller passes the home and no chat folder, so an unscoped chat keeps today's view. LayoutPrompt takes a ScopedPrompt. A scoped chat hears only of its folders: the agent's folder by its name and mode, with a line that it cannot see the user's other files, the chat folder and where its working files go, and the shared folders. It gets no hint to build a page, because pages live outside its folder. An unscoped chat keeps today's text. Assisted-by: Claude:claude-opus-5-5
A caller that keeps a node ID, such as the folder of a saved agent, had no way to ask the store where that node is now. It had to search the journal and then list folders, which costs one query per folder in a large home. Store.NodeByID returns a live node at its current path, so a rename or a move of the node or of a folder above it is followed. It answers ErrNotFound when the node or a folder above it is deleted, and when the actor cannot read it; a fenced actor finds only nodes inside its fence. Both stores implement it and the shared suite checks it. The remote store of the Sandbox add-on refuses it, like its other calls. Assisted-by: Claude:claude-opus-5-5
A chat whose agent has a folder keeps its chat folder in the home, but sees it as a mount of its own at /workspace/chat. Its work folder is then /workspace/chat/work, not a path under /workspace/home. workfiles reads /workspace/chat as the chat folder: a helper file under its work folder is working, a script beside the attachments is a stray helper, and a folder named chat in the agent's folder is an ordinary folder. ScopedPrompt steers the model to that work folder and sends the deliverables to the agent's folder, or to the chat folder when the agent may only read. It says nothing of pages or of the settings folder, which are outside the agent's folder. The chat picks the text by its view. Assisted-by: Claude:claude-opus-5-5
The folder resolver searched the journal of the home and then listed folders to find where an agent's folder is now. The stores can now find a node by its ID, so the resolver asks them directly and the search is gone. Assisted-by: Claude:claude-opus-5-5
The file tools mapped paths to the user's home and acted as the chat's actor, and the media tools did the same through files.HomePath. A chat whose agent has a folder must reach only its folders, and search must start in the agent's folder, so the count of documents still being indexed cannot count files elsewhere in the home. files.Service.Tools and media.Service.Tools take a session.Access. Every path goes through its place and every call acts as its actor. A write to a read-only folder is refused before the store sees it. A search with no path starts at the primary folder, and in a scoped chat also looks in the chat folder; the pending count is taken below that root. files.HomePath is removed. UnscopedAccess builds today's access of a chat (the home and the shared folders), and the studio service takes the turn's access from one function, turnAccess, which gives that access until the chat access resolver replaces it. The new specs run the paths that try to leave the folder (parents, absolute and doubled paths, encoded and look-alike separators, NUL, other cases) through every file tool and the media tools, and check that nothing outside the folder is read, named or changed, and that search counts no pending file outside it. Assisted-by: Claude:claude-opus-5-5
A chat whose saved agent has a folder must reach only that folder. Up to now every chat got the user's actor, which reaches the whole home. The chat's access is now resolved from the stores at every turn start. With no agent folder it is the actor and the view of today. With a folder the actor is fenced to the agent's folder at its mode, the chat's own folder read and write, and the shared folders and the skill layers read only; the chat's folder is made before the fence, because a fenced actor cannot make it. A sub-agent takes the folder of its parent's agent, and its own agent's folder inside it. A folder that is deleted, unreadable or not chosen refuses the chat with ErrAgentFolderMissing; it never falls back to the whole home. A chat records whether its agent had a folder, so a chat whose agent was deleted is refused too. CreateAs refuses such an agent at once. session.Access carries the actor and the view, has no exported fields and one constructor. Open takes it, the turn tools are built from it, and an open chat compares the fence as well, so an edit of the agent restarts the harness at the next turn on any frontend. The person's uploads and renames use the owner's actor, without the fence. The files a command changed are read as the chat can read them. Nothing is kept between turns, so frontends cannot disagree. Assisted-by: Claude:claude-opus-5-5
The turn prompt lists the chat's files with the paths the model uses. For a chat whose agent has a folder those paths come from its mounts, and a file the chat touched outside its folders, for example before the agent got a folder, must not be named. Manager.FilesIn is Files with a place. For a scoped place, each file is named by its mount (/workspace/home for the agent's folder, /workspace/chat for the chat folder) and a file outside every mount is left out. Other places give what Files gives. The prompt's file list uses the turn's place; the chats API keeps the user-wide list. Assisted-by: Claude:claude-opus-5-5
A chat with a saved agent whose folder is missing or not chosen does not start. The chat endpoints now say so with 409 and the type agent_folder_missing, both when the chat is created and when a message is sent to an open chat, instead of a server error. The agent docs say what a chat with a folder reaches, when a change of the folder applies, and that nothing is kept between turns. Assisted-by: Claude:claude-opus-5-5
The rules nudge walked from the chat folder up to the home root and named AGENTS.md files there, and the stray-helper note named the work folder under /workspace/home. A chat whose agent has a folder must hear only of its own folders. The nudge takes the turn's access: it reads paths through the place, starts at the primary folder (the agent's folder) and stops there, so it never points above the folder or into the chat folder mount. The helper note names /workspace/chat/work/ in a scoped chat, and a sub-agent of a scoped parent gets the scoped work folder text. Assisted-by: Claude:claude-opus-5-5
An actor built from a principal has no fence and reaches the whole home. A spec now parses the studio service, the command runners, the add-on host operations and the application wiring, and fails on a workspace.Actor literal, a files.UserActor call or an Actor(principal) call in a function that is not on the allowlist. Each allowed entry says why: a host read of the user's settings, a user surface, or a caller that a later change moves to the chat's access. An entry that builds no actor any more fails the spec too, so the list stays exact. Assisted-by: Claude:claude-opus-5-5
Assisted-by: nib:gpt-6.1-sol
The spec of a chat without an agent folder compared its view with BuildView called with the old arguments, so the test package did not compile after the merge of the view builder. It now passes the home as the primary folder and no chat folder, which is the view of an unscoped chat. Assisted-by: Claude:claude-opus-5-5
The resolver of a chat's access built the view of a scoped chat itself, with no shared folders, while every other chat got its view from the session manager. And the file tools, the media tools, the chat's file list in the prompt, the rules nudge and the work folder lines each asked for an access of their own, the view of the whole home. A chat whose agent has a folder therefore had a fenced actor but home-relative paths: it reached its folder only through paths such as /Projects/Acme/a.txt, and its prompt named the home. The session manager now builds every chat's view: UserAccess mounts the home, and the new ScopedAccess mounts the agent's folder at home, capped at the fence's mode, the chat folder at chat and the shared folders read only. It refuses an actor without a fence, and a folder outside the fence is an error, never a view of the home. The resolver calls it with the innermost folder, so a sub-agent of a saved agent still works in its own folder. The access that the resolver builds once at the start of the turn now goes down to every consumer. A sub-agent of a saved agent acts for its parent chat with the same access (Access.ForChat), and a generic sub-agent gets its tools from the parent's access. The seam that gave the unscoped access, and the files service's own view builder, are removed, so a new consumer cannot build a view by mistake. A turn whose access has no folder gets no tools rather than the home. A scoped chat now sees its folder at /workspace/home and its chat folder at /workspace/chat in its tools, its search, its prompt and its work folder lines. A chat whose agent has no folder is unchanged. Assisted-by: Claude:claude-opus-5-5
The turn's access is resolved once and handed down, but nothing stopped new code from building a view of its own, which shows a scoped chat the whole home. A second scan now parses the same sources as the actor scan and fails on a call of BuildView, SkillLayerMounts, NewAccess, UserAccess or ScopedAccess, or on a store's View, in a function that is not on its allowlist. The resolver is the one allowed caller; the host bash, the sandbox runner and the add-on host operations are listed until they take the chat's access. A stale entry fails the scan too. Assisted-by: Claude:claude-opus-5-5
A saved agent started from a scoped chat could name a folder outside the chat's own, and the child was made before its first turn failed with a message about a missing folder. spawn_agent now checks the agent against the chat's agent from the stores before it makes the child: no folder is fine, the same folder or one inside it is fine, anything else fails with "this agent works in another folder than this chat". The child never has more reach than its parent, in folder or in mode. The ScopedAgent flag of a sub-agent was reset to what its own agent had at each turn. A child of a scoped chat with an agent without a folder lost the mark, so deleting the parent's agent would have given that child the whole home. The flag now follows both folders. New specs cover a generic child, a saved-agent child outside, inside and at the parent's folder with the lower mode, a child without a folder, and the work folder wording of a child of a read-only or read-write parent. Assisted-by: Claude:claude-sonnet-5-5
list_agents gave a scoped chat every saved agent, including ones that spawn_agent would refuse, and told the model nothing about where an agent works. Each entry now carries the name of the agent's folder and its mode, or folder_missing when the folder is deleted or not chosen. The path is left out, since it would show the folders above the chat's own. A chat whose agent has a folder lists only the agents that fit under that folder, by the same check spawn_agent makes. The docs for saved agents as sub-agents and for the agent folder describe both. Assisted-by: Claude:claude-sonnet-5-5
A chat of an agent with a folder reaches only that folder, but its triggers still worked on the whole home. A file trigger could watch any folder, the rules of a run were read from the home root down with the user's actor, the first message named home paths, and a run whose agent's folder was gone fell through to a failed chat start after the host had already read the home's rules. The trigger tools of a scoped chat could also list, change or propose triggers that reach the whole home. The agent's folder now bounds every step: - Saving a file trigger (create, update, edit, confirm) refuses a folder outside the agent's folder, and a trigger of an agent whose folder is missing or not chosen. - Each run checks the agent's folder before it reads anything. A missing or unset folder skips the run with "the agent's folder is missing" and switches the trigger off; a watched folder that is not inside the agent's folder does the same with its own reason. No chat is made and the owner is told. - The folder check and the rules are read with an actor fenced to the agent's folder, read only, so no AGENTS.md above it is read. The rules of a schedule run start at the agent's folder. The first message names folders and the file as the chat sees them, under /workspace/home, and the rules nudge of the run's chat counts those rules as given. - In a scoped chat the trigger tools read folders as paths of the chat's place, refuse one outside the agent's folder, and see only the triggers of that agent. The trigger watcher takes the service's administrator actor, made once next to the change feed's, and the trigger functions leave the actor scan's allowlist. Assisted-by: Claude:claude-opus-5-5
The files a connector reply sends were listed from every change made in the chat's name and read with the user's actor, so a reply of an agent limited to one folder could send a file from anywhere in the home that some host-side write had attributed to the chat. A message to an agent whose folder was missing made or reopened a chat, failed to start it, and answered with a generic error. A reply now resolves the chat's access once, lists the turn's files through its place (a file outside the chat's folders is left out) and reads each one with its fenced actor. A file the access cannot reach is skipped without being named. Before it makes a chat, a message checks the bound agent's folder; when it is missing or not chosen, the sender gets one reply, "This agent cannot work right now: its folder is missing.", the refusal is logged and the binding stays on. The same reply covers a folder that goes away between the check and the turn. readChatFile leaves the actor scan's allowlist. Assisted-by: Claude:claude-opus-5-5
The store names an upper entry that a read-only mount refused by its path in the view (/home/x), not by its store path. The note that a command's output ends with took every path but an outside-view one for a store path, so it told the model that "/home/x was not saved", a path the guest does not have. A path that no mount holds but that starts with a mount name is now taken as a path of the view and gets the /workspace prefix. Assisted-by: Claude:claude-opus-5-5
The host bash and the sandbox runner made their own actor and view: the host bash a user actor from the principal and the view of the whole home, the sandbox runner a plain actor by default and the view from its own mount requests. Neither knew the chat's agent folder, so a command of a scoped chat could reach the whole home. The runner now has no default actor or view. Options.Actor takes a context and returns an error, and Options.View returns the view itself; both are asked at every call. A view of another actor than the chat's is refused, and a sandbox whose actor or mounts changed is made again, so an agent's new folder or mode applies from the next command. A failing resolver fails the call, also with a sandbox running. The server wires both hooks to the chat's resolved access: the new Service.CommandActor and Service.CommandView, the latter refusing an actor that is no longer the chat's. Access.SandboxView adds the skill layers to the turn's view, at the names the file tools use. The host bash refuses a fenced actor, at the tool and again at each call, as a command on this machine reads what the server can read. The Sandbox add-on passes a label actor and its home view: the host binds each workspace operation to the call's chat. The lane 5 entries leave the actor and view scan allowlists. Assisted-by: Claude:claude-opus-5-5
A command of the built-in bash runs on this machine and reads every file LocalAI can read. No workspace rule confines it, so a chat whose agent works in one folder cannot have it, whatever chats_host_exec says. The tool's build now refuses a fenced actor, so the chat gets no bash, its prompt says nothing runs commands, and the shell group of the Tools menu gives the reason: the agent works in one folder and runs commands only in the Sandbox add-on. Sub-agents of such a chat have the fenced actor too. The docs of saved agents and of the commands on this machine state the rule and the Sandbox layout of a scoped chat. Assisted-by: Claude:claude-opus-5-5
The agent form gets a Files section after the instructions: All my files, or One folder chosen in the folder picker, and whether the agent's chats can read the folder or read and change it. The picker shows the folders LocalAI manages as ones that cannot be chosen, and Home itself can be chosen. A folder that was deleted, or an imported agent whose folder was not found, is said in place with an offer to choose another; the rest of the form still saves and keeps it. A save sends the folder only when the person changed it, so editing another field never touches it. Picking One folder and choosing nothing stops the save, as it would otherwise save an agent on all the files. The Export section now downloads the file the server writes, which names the folder, and says so. Assisted-by: Claude:claude-opus-5-5
An agent with a folder shows it in the list's line and on its page, with its access in words and a lock when its chats can only read it. A folder that is gone, or an agent with no folder chosen yet, shows a warning that opens the form; the agent's page also says so above its facts and offers to choose another folder. Assisted-by: Claude:claude-opus-5-5
A folder that a saved agent works in gets a small marker with the agent's name, or the number of agents, and a tooltip that names each one with its access. Inside such a folder, a line over the list says the same. The page reads the agent list it can already fetch, so the files API is unchanged, and the page still shows all the files. Assisted-by: Claude:claude-opus-5-5
A chat with an agent that has a folder shows the folder next to the agent in the message box, with a lock for read only, and the Tools menu says which files the chat's tools reach. When the folder is gone, the chip says so and opens the agent's form. A chat whose agent's folder is missing does not start, and the server answers with a conflict. The message box now says that in plain words, keeps the message, and offers Edit agent, instead of the server's text. Assisted-by: Claude:claude-opus-5-5
The agent stubs now list folders by path, read a written folder back as the server does, export a version 2 file for an agent with a folder, import one whose folder is not found as unset, and refuse to start a chat whose agent's folder is missing. The specs choose a folder and see it on the list, switch the access, leave an unchanged folder out of the save, clear it, keep and replace a missing folder, keep Chats and LocalAI out of the picker, retry a folder that cannot be read, see the Files marker, the chat's chip and Tools line, the missing-folder chat error with Edit agent, and the import warning. A render smoke opens the picker on the real server, and one spec checks that the copy names no internal path. Assisted-by: Claude:claude-opus-5-5
The agent docs said what the folder field does in the API. They now also say where the web interface sets it, how the agents pages, the Files page and a chat show it, and what a chat whose folder is missing offers. Assisted-by: Claude:claude-opus-5-5
An add-on's workspace operations run as the user of the live request, with no idea which chat made it. Only an action carries a chat, and only as a key in its resource ID; a read tool such as the Office add-on's read-docx carries none. So a chat that should reach only one folder could reach the whole home through an add-on. The in-flight record of a request now keeps, next to the envelope, a binding to the chat the request was made for: the user's principal and the chat ID. It is host-side only and never a field of the envelope, which installed add-ons decode with unknown fields refused. A context carries the binding from the chat turn to the place that records the request. The broker gives each host operation the chat and the scope of its live request. An operation ID that requests of two chats share is not live, so neither chat is trusted. Assisted-by: Claude:claude-opus-5-5
The runtime client records each call's envelope for the host operation channel. It now records the chat binding too when the call's context carries one, so the add-on's host operations know which chat made the request. The envelope sent to the add-on is the same bytes with or without a binding: a spec checks that the body of a bound call equals the body of an unbound one and still decodes with unknown fields refused. Assisted-by: Claude:claude-opus-5-5
A frontend that is not the add-on owner forwards an add-on call in a signed request. The chat the call was made for now rides in that request as a signed field, chat, and the owner puts it back on the context of the call it serves, where the runtime client records it. A changed, added or removed binding fails the signature check, and a signed but incomplete one is refused. A request that no chat made has no chat field and signs as before. An owner that does not know the field refuses a forwarded call that a chat made, so the owner must be upgraded before the frontends. Assisted-by: Claude:claude-opus-5-5
The chats consumer ran an add-on tool with the conversation ID only, which the owner used to resolve a selection and, for an action, as the key of the add-on's state. Nothing told the host which chat a read tool or a change came from. The tool adapter now has ExecuteChat, which takes a ChatCall (the chat's user and ID, from the chat turn's own record) and binds the call to the chat on the host side for every tool kind: a read with no resource, an action, and a change made at once. The chats adapter refuses a call that names a conversation without that binding, so a chat's call cannot be made unbound by mistake. Other consumers, such as the OpenAI-compatible route, keep calling without a binding. Assisted-by: Claude:claude-opus-5-5
The workspace operations of add-ons (materialize, commit, changes, export and import) worked on the whole home of the request's user. A chat whose agent has a folder could therefore read and write any file through the Office or Sandbox add-on. They now resolve their access from the chat binding of the live request, through a new Access hook that the chats service answers (Service.AddonAccess). A chat's call gets the chat's access, as its file tools do: in a chat whose agent has a folder, the add-on's "/" is that folder, /workspace/chat names the chat folder, the agent's mode applies, and paths resolve through the chat's place, so a parent reference is refused and an encoded separator is part of a name. The mounts go through Access.SubView, which builds a view only inside the access, for its fenced actor. When an add-on mounts its "/" at home, the host adds the chat folder at chat, so the Sandbox sees the chat's layout without a change. Changes list only what the access may read, and a commit entry outside the view is a conflict. A call whose chat cannot be resolved is refused and never runs user-wide. An action request without a binding is resolved by its chat key, as before, and refused when it names no chat. A call that no chat made (an add-on page, the OpenAI-compatible route) still works on the whole home, through Manager.PersonAccess. The four lane 6 entries leave the actor scan and the view scan. PersonAccess joins the view scan's matcher, with AddonAccess as its one allowed caller. An end-to-end spec runs the real Office and Sandbox add-ons in a scoped chat. Assisted-by: Claude:claude-opus-5-5
files.Service.Tools takes a session.Access since the chat access change, so the route test of the text index passed a bare actor and its package did not compile under the auth tag. The test now builds the access as the chats service does for a chat whose agent has no folder: the user's home as the primary folder of the view, then session.NewAccess. The assertions are unchanged. Assisted-by: Claude:claude-opus-5-5
The specs of the folder feature each test one surface. This suite runs one chat of an agent limited to a folder through all of them at once, on an indexed SQL workspace: the file and media tools, the search and its pending count, a command of the Sandbox in a fake sandbox, a read call and an action call of an add-on through the real workspace operations bound to the chat, a generic and a saved sub-agent, the trigger tools, a file trigger run, a connector reply, a renamed folder, a mode change, an edit that narrows the folder (also seen by a second frontend) and a deleted folder. The files outside the folder hold a token that the scripted model never writes. After each spec the suite collects every text a person or a model can see: tool results and errors, prompts, messages, transcripts with their cards, live events, chat file lists, tool panels, notifications, connector replies and files, the add-on's data directory and the connector log. The token must be in none of them, and the files outside must be unchanged. A control spec runs the same probes from an agent without a folder and checks that they do find the token, so the probes and the collection cannot pass by seeing nothing. Assisted-by: Claude:claude-opus-5-5
Each change of the folder feature documented its own part. This makes the pages one account of it: - Saved agents: the missing and unset states in one bullet, what the folder does not limit (MCP servers, web tools, collections, pages), that the Files page stays user-wide, when an edit applies, the downgrade caveat, and the folder as a stateless row of the cluster table. The Sandbox of a scoped chat mounts the agent's folder and the chat folder only, as the add-on asks for them; the page said it also had the shared folders and skills. - Chats: the chat folder at /workspace/chat for the model, sub-agents of a scoped chat, and commands only in the Sandbox. - Add-ons: the Sandbox and Office sections say what / is in a chat whose agent has a folder. - First agent: the optional folder step and the "Folder missing" case. Assisted-by: Claude:claude-opus-5-5
No release notes file exists for the next release yet, so the draft text lives next to the spec and the plan. It carries the release note paragraph with the downgrade caveat and the upgrade order of a cluster. Assisted-by: Claude:claude-opus-5-5
The chat binding change added five specs to the gateway and runtime packages and named them in the case lists of the gateway, artifacts and apps suites. These three suites also keep one case list per Ginkgo report, and the verifier compares each report with its own list. The new names were missing there, so the three suites failed with an inventory mismatch. A dry run of every Ginkgo suite's commands now lists the same It names as each report's inventory, in both directions. Assisted-by: Claude:claude-opus-5-5
A folder moved into a fence makes its parent identities readable. Their historical paths can still name locations outside the fence. Hide each historical side when its readable parent's current path differs. Reuse the generic title when filtering hides a side. Keep the journal and unfenced history unchanged so Undo uses identities. Assisted-by: nib:gpt-6.1-sol
The Tables tools runner includes every adapter spec in its output. List the four chat binding cases so the exact inventory accepts them. Assisted-by: nib:gpt-6.1-sol
A followed folder outside the home cannot round-trip through a home path. Export its mode with an empty path so import warns and stays fail-closed. Responses keep public shared paths without exposing other internal paths. Distinguish deleted scoped agents from unscoped agents in the guidance. Assisted-by: nib:gpt-6.1-sol
Keep every required shared root in each intersecting fence. Refuse creation and turn access when the complete set exceeds 32 roots. Reserve a chat root before its folder exists and fence listings empty on refusal. Propagate shared enumeration failures instead of omitting roots. Map capacity refusals to a fixed 409 response and give connectors a safe capacity message. Check boundaries, retries, background access, and HTTP handlers with focused regressions. Assisted-by: nib:gpt-6.1-sol
Assert the folder-specific refusal without a harness start or saved chat. Clarify public response paths and unset exports in the API metadata. Assisted-by: nib:gpt-6.1-sol
A background refresh consumes the one-shot successful response before this test observes its load error. Isolate the mocked Files page from the live workspace stream and agents list. Keep reads failing until the error assertions pass, then allow the actual Retry click to recover. Check that Retry reads again and removes the error, without changing the existing assertions or their timeouts. Assisted-by: nib:gpt-6.1-sol
Use the existing Ginkgo suites for capacity regressions. Exercise a successful turn, refusal after shares change, and a retry through a second service on the same stores. Check that refusal leaves no claim or pending message. Verify that connectors send one fixed refusal and that triggers stay enabled after capacity failures. Document the 32-root budget and the all-required-roots-or-refuse contract. Assisted-by: nib:gpt-6.1-sol
mudler
marked this pull request as ready for review
November 1, 2026 14:05
mudler
previously approved these changes
Nov 1, 2026
The crash fixture exits after 100 ms, before startup necessarily finishes. A busy runner can fail the readiness handshake instead of testing restarts. Start a normal fixture and kill each published incarnation explicitly. Keep the exact restart count and persistent failed-state assertions. Assisted-by: nib:gpt-6.1-sol
Assert service and workspace cleanup through Ginkgo. Keep service cleanup before workspace cleanup and report close failures instead of discarding them. Assisted-by: nib:gpt-6.1-sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Introduce folder-scoped saved agents: choose a workspace folder and access mode for an agent, and carry that scope into its chats and tool calls. This draft opens the existing implementation for review before the remaining implementation and final verification are complete.
The current branch includes:
docs/content/, plus the design and implementation plan.No linked issue is asserted for this draft.
Notes for Reviewers
This is an initial draft, not a merge-ready security or completeness claim. Please review the approach while follow-up work proceeds.
git diff --check, identical repository trees before and after merge-message privacy cleanup, and targeted private-workspace-reference scans of the feature history and introduced blobs. These checks do not replace functional or security testing.Assisted-by: nib:gpt-6.1-sol. No source or documentation contents changed during that cleanup. Rewritten commits cannot retain valid original cryptographic signatures; signature/sign-off review remains with the human submitter.Signed commits