Files
LocalAI/core/schema/chats.go
T
mudler-agentandEttore Di Giacinto b39d37f559 feat(agents): limit saved agents to one folder (#13009)
* Store the folder and its mode on a saved agent

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

* Keep on a chat whether its saved agent had a folder

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

* Add the design and plan for agent folder scope

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

* Resolve the folder of a saved agent by node

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

* Add a fence to the workspace actor

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

* Show and choose the folder of an agent in the API

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

* Enforce the actor fence in both workspace stores

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

* Map the model's paths through the chat's place

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

* Build a chat's view from its folders and describe it

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

* Find a workspace node by its ID

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

* Name the work folder of a scoped chat at /workspace/chat

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

* Look up the folder of an agent by node

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

* Resolve the file and media tools through the chat's access

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

* Resolve the access of a chat at every turn

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

* List a scoped chat's files by its place in the prompt

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

* Answer 409 when an agent's folder is missing

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

* Point a scoped chat at rules and work folders in its own folders

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

* Find workspace actors built outside the chat access

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

* Build the expected view of a chat with the view's folders

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

* Give every file tool of a turn the chat's resolved access

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

* Find chat views built outside the chat access

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

* Keep a sub-agent inside its parent's folder

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

* Show agent folders in list_agents and limit them by scope

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

* Keep the triggers of a scoped agent to its folder

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

* Send connector replies through the chat's access

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

* Name a read-only refusal of a commit by its guest path

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

* Take the actor and view of command runners from the chat's access

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

* Offer no host bash to a chat whose agent works in one folder

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

* Tell a scoped chat where its Sandbox commands start and what they keep

The commands block of the prompt told every chat with the Sandbox
that a command starts in /workspace/home, the user's home, and that
what it saves there or in a writable shared folder is kept. In a
chat whose agent works in one folder, /workspace/home is that folder,
the chat folder is /workspace/chat and the shared folders are read
only. The block now says so, and with a read-only folder it names
only /workspace/chat as kept.

Assisted-by: Claude:claude-opus-5-5

* Name a scoped chat's attachments where its commands see them

The model learns of an attached file by its guest path, which was
always /workspace/home/Chats/<chat>/<file>. In a chat whose agent
works in one folder, /workspace/home is that folder: the file and
media tools still found the file through the place's chat alias, but
a command sees the chat folder only at /workspace/chat, so the path
named nothing for it.

The text the model gets now names a file of the chat folder at
/workspace/chat/<file> in a scoped chat, through the chat's place:
when a message is sent, queued or injected, in the summary of the
chat, and in the model state rebuilt after an edit, a regenerate, a
fork or an import. What is stored keeps the home spelling, which
SplitMessage reads back, and an unscoped chat is unchanged.

Assisted-by: Claude:claude-opus-5-5

* Remove the duplicated allowlist entries of the actor scan

Assisted-by: Claude:claude-opus-5-5

* Read and write the folder of an agent in the agent helpers

The agent API now answers an agent's folder with its path, mode and
state, and a write keeps the stored folder unless it names one. The
page helpers learn that shape: the folder field of a save is sent only
when the person changed it, an empty path clears it, a missing or
unset folder that the person did not replace is kept, and the mode is
read unless write was chosen. Small helpers say whether a listing
entry can be chosen (not a folder LocalAI manages, not a shared
folder), which folders agents use for the Files page marker, and
whether a chat error is the missing-folder conflict.

Assisted-by: Claude:claude-opus-5-5

* Move the folder walk of the Move dialog into a folder picker

The agent editor needs the same walk through the person's folders as
the Move dialog, so the breadcrumb and the folder list move into
FolderPicker, with their styles, and the Move dialog draws it. The
picker can show a folder as one that cannot be chosen, with the
reason next to it, and a folder that cannot be read now offers to try
again; a folder still loading says so instead of an empty list.

Assisted-by: Claude:claude-opus-5-5

* Choose the folder of an agent in the agent form

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

* Show the folder of an agent on the agents list and its page

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

* Mark the folders that agents work in on the Files page

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

* Show a chat's folder and say when its agent's folder is missing

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

* Test the folder of an agent across the web interface

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

* Describe the agent's folder in the web interface

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

* Record the chat an add-on call was made for

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

* Keep the chat binding of a runtime call on the host

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

* Carry the chat binding in a forwarded add-on request

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

* Bind every add-on tool call of a chat to the chat

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

* Run add-on workspace operations with the chat's access

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

* Build the chat access of the file index route test

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

* Test the folder of an agent through every surface of a chat

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

* Complete the docs of the folder of an agent

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

* Draft the PR description and release note of agent folders

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

* List the chat binding specs in the per-report add-on inventories

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

* Hide stale paths from a fenced actor's history

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

* List the chat binding cases in Tables tools

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

* Keep shared agent folders unset in exports

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

* Refuse scoped chats over the folder limit

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

* Verify unset imports refuse chat startup

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

* Keep the folder retry test under user control

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

* Test and document folder capacity refusals

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

* Control crashes in the restart budget test

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

* Check capacity test cleanup errors

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

---------

Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-01 18:30:08 +01:00

1692 lines
69 KiB
Go

package schema
import (
"encoding/json"
"time"
)
// ChatsCreateRequest starts a chat.
type ChatsCreateRequest struct {
// Title is optional; the conversation is named after its first message otherwise.
Title string `json:"title,omitempty"`
// Model is the model the conversation talks to. It cannot change later.
Model string `json:"model"`
// AgentID starts the chat with one of your saved agents. The agent's model,
// when it has one, replaces Model, which is then optional.
AgentID string `json:"agent_id,omitempty"`
// Assistant starts the chat with the LocalAI management tools (install
// models and backends, change settings). It needs an administrator and the
// assistant setting on, and it cannot be combined with AgentID.
Assistant bool `json:"assistant,omitempty"`
// Switches are the tools, add-ons and skills the chat starts with
// switched off (and the opt-in tools switched on), as the composer of a
// new chat drafts them. They are stored with the chat: the chat never
// exists without them. A bad entry refuses the whole request.
Switches *ChatsSwitches `json:"switches,omitempty"`
}
// ChatsSwitches are the switches of a chat, each list in the form its own
// PUT endpoint takes: add-on IDs, MCP server names and connector:<id>,
// groups of built-in tools, opt-in groups that are on, and skill ids.
type ChatsSwitches struct {
AddonsOff []string `json:"addons_off,omitempty"`
McpOff []string `json:"mcp_off,omitempty"`
BuiltinsOff []string `json:"builtins_off,omitempty"`
BuiltinsOn []string `json:"builtins_on,omitempty"`
SkillsOff []string `json:"skills_off,omitempty"`
}
// ChatsAgent is a saved agent. The four lists are exact grants: an empty list
// grants nothing, and a chat gets only what you may use at that moment.
type ChatsAgent struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Instructions string `json:"instructions"`
Model string `json:"model,omitempty"`
MCPServers []string `json:"mcp_servers"`
Addons []string `json:"addons"`
Collections []string `json:"collections"`
Skills []string `json:"skills"`
// Network is the network preset of the Sandbox commands of the agent's
// unattended runs: off (the default), registries or all, capped by the
// administrator's ceiling.
Network string `json:"network"`
// Folder is the one folder that chats with the agent work in. It is
// absent for an agent that works on all your files.
Folder *ChatsAgentFolder `json:"folder,omitempty"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// ChatsAgentFolder is the folder of a saved agent as it is now. The server
// keeps the folder itself, not its path, so a renamed or moved folder shows
// its new public path when one is available.
type ChatsAgentFolder struct {
// Path is home-relative ("/" is the whole home), or /shared/<name>/...
// for a followed readable shared folder. It is absent when missing,
// not chosen, or readable outside these public locations; a readable
// folder without a public path still has state ok.
Path string `json:"path,omitempty"`
// Name is the folder's own name; absent for "/".
Name string `json:"name,omitempty"`
// Mode is read (the agent's chats can read the folder) or write (they can
// read and change it).
Mode string `json:"mode"`
// State is ok; missing when the folder was deleted or you can no longer
// read it; or unset when no folder is chosen yet (an imported agent whose
// folder was not found). An agent whose folder is missing or unset does
// not start.
State string `json:"state"`
}
// ChatsAgentFolderWrite chooses the folder of a saved agent.
type ChatsAgentFolderWrite struct {
// Path is a folder of your files, "/" based; "/" is all of them, read
// only or not. It cannot be LocalAI or Chats, a folder inside them, or a
// shared folder. An empty path clears the folder.
Path string `json:"path"`
// Mode is read or write; it is required with a path.
Mode string `json:"mode,omitempty"`
}
// ChatsAgentWrite creates or replaces a saved agent.
type ChatsAgentWrite struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Instructions string `json:"instructions,omitempty"`
Model string `json:"model,omitempty"`
MCPServers []string `json:"mcp_servers,omitempty"`
Addons []string `json:"addons,omitempty"`
Collections []string `json:"collections,omitempty"`
Skills []string `json:"skills,omitempty"`
Network string `json:"network,omitempty"`
// Folder chooses the one folder the agent's chats work in. A write
// without it keeps the folder the agent has; {"path": ""} clears it. In
// an export file it is the home-relative path and mode when the file was
// made. A missing, unchosen or non-home folder has an empty export path;
// import preserves its mode as unset and warns instead of clearing scope.
Folder *ChatsAgentFolderWrite `json:"folder,omitempty"`
}
// ChatsAgentList is the response of the agent list endpoint.
type ChatsAgentList struct {
Agents []ChatsAgent `json:"agents"`
}
// ChatsConversation is a chat and the state of its turn.
type ChatsConversation struct {
ID string `json:"id"`
Title string `json:"title"`
Model string `json:"model"`
Kind string `json:"kind"` // chat or subagent
ParentID string `json:"parent_id,omitempty"`
// AgentID is the saved agent the chat runs with. Origin is trigger or
// connector for a chat that started by itself, and OriginID names the trigger
// or binding.
AgentID string `json:"agent_id,omitempty"`
Origin string `json:"origin,omitempty"`
OriginID string `json:"origin_id,omitempty"`
// Assistant is true for a chat that has the LocalAI management tools.
Assistant bool `json:"assistant,omitempty"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
// Running is true while a turn runs. LastTurn is how the last finished turn
// ended: done, stopped, failed, interrupted, or paused (the tool loop
// reached its step limit; send a message to go on).
Running bool `json:"running"`
LastTurn string `json:"last_turn,omitempty"`
// Settings are the chat's own model settings, when it has any.
Settings *ChatsSettings `json:"settings,omitempty"`
// Goal is the chat's goal, when it has one.
Goal *ChatsGoal `json:"goal,omitempty"`
}
// ChatsGoal is the persistent objective of a chat. While State is active the
// server starts the next turn by itself when one ends, up to MaxTurns
// automatic turns and MaxSteps model steps.
type ChatsGoal struct {
Text string `json:"text"`
// State is active, paused, done or blocked.
State string `json:"state"`
// PausedBy says why a paused goal paused: user, turn_limit, step_limit,
// error or interrupted.
PausedBy string `json:"paused_by,omitempty"`
// Note is the model's summary of a done goal, or the reason of a blocked
// one.
Note string `json:"note,omitempty"`
// Turns and Steps count the automatic turns and the model steps since the
// goal was set or last resumed; MaxTurns and MaxSteps are the limits that
// apply now (chats_goal_max_turns, chats_goal_max_steps).
Turns int `json:"turns"`
MaxTurns int `json:"max_turns"`
Steps int `json:"steps"`
MaxSteps int `json:"max_steps"`
Set time.Time `json:"set"`
Updated time.Time `json:"updated"`
}
// ChatsGoalRequest sets the goal of a chat.
type ChatsGoalRequest struct {
// Text is the goal, 1 to 2000 characters.
Text string `json:"text"`
}
// ChatsGoalResponse is the goal of a chat after a request. Goal is null when
// the chat has none. Replaced is the goal that a PUT replaced, or that a DELETE
// removed.
type ChatsGoalResponse struct {
Goal *ChatsGoal `json:"goal"`
Replaced *ChatsGoal `json:"replaced,omitempty"`
}
// ChatsConversationList is the response of the list endpoint.
type ChatsConversationList struct {
Conversations []ChatsConversation `json:"conversations"`
// Persistent is false when conversations live in memory and are lost when
// the server restarts.
Persistent bool `json:"persistent"`
// Files is false when the workspace is off; FilesReason then says why.
Files bool `json:"files"`
FilesReason string `json:"files_reason,omitempty"`
// ImageModel is the model that describes the images of a chat whose own
// model cannot look at them. It is empty when no model can.
ImageModel string `json:"image_model,omitempty"`
}
// ChatsConversationDetail is a conversation with the messages waiting for the model.
type ChatsConversationDetail struct {
ChatsConversation
Pending []ChatsPending `json:"pending"`
// HostConsent is the card of a command of the built-in bash that waits for
// a yes while a turn runs; answer it with POST /api/chats/{id}/host-exec/consent.
HostConsent *ChatsHostConsent `json:"host_consent,omitempty"`
// Loops are the loops of the chat, oldest first (GET /api/chats/{id}/loops).
Loops []ChatsLoop `json:"loops"`
}
// ChatsLoop is a loop of a chat: a prompt the chat sends to itself again, at
// an interval or when the model asks for the next run.
type ChatsLoop struct {
ID string `json:"id"`
Prompt string `json:"prompt"`
// Interval is the time between runs, such as 5m; empty for a self-paced
// loop, which runs again when the model asks.
Interval string `json:"interval,omitempty"`
// IntervalSeconds is Interval in seconds; 0 for a self-paced loop.
IntervalSeconds int64 `json:"interval_seconds"`
// State is pending (waits for your yes), active, paused or ended.
State string `json:"state"`
// Reason says why the loop is paused or ended, or why it waits.
Reason string `json:"reason,omitempty"`
Runs int `json:"runs"`
// Failures counts the failed runs in a row; three pause the loop.
Failures int `json:"failures,omitempty"`
// Next is when the next run is due; absent while a run goes on or the
// loop does not run.
Next *time.Time `json:"next,omitempty"`
// Running is true while a run of the loop goes on in the chat.
Running bool `json:"running"`
// Wait is what the model of a self-paced loop said it waits for.
Wait string `json:"wait,omitempty"`
Created time.Time `json:"created"`
LastRun *time.Time `json:"last_run,omitempty"`
}
// ChatsLoopRequest starts a loop in a chat.
type ChatsLoopRequest struct {
// Interval is the time between runs, such as 30s, 5m or 2h. Leave it
// empty for a self-paced loop.
Interval string `json:"interval,omitempty"`
// Prompt is plain text, or a skill: /skill-name and its words.
Prompt string `json:"prompt"`
}
// ChatsLoopStateRequest pauses a loop or makes it active.
type ChatsLoopStateRequest struct {
// State is paused or active. Making a pending loop active confirms it.
State string `json:"state"`
}
// ChatsLoopList is the loops of a chat.
type ChatsLoopList struct {
Loops []ChatsLoop `json:"loops"`
}
// ChatsLoopMark names the loop run that sent a user line.
type ChatsLoopMark struct {
ID string `json:"id"`
Run int `json:"run"`
}
// ChatsHostConsent is a command that waits for the person's answer.
type ChatsHostConsent struct {
ID string `json:"id"`
Command string `json:"command"`
}
// ChatsSendRequest is a message to a conversation.
type ChatsSendRequest struct {
Text string `json:"text"`
// Context adds MCP prompts and resources to this message as context. Each
// is expanded on the server, as you, from a server the chat can use.
Context []ChatsMCPRef `json:"context,omitempty"`
// Skill runs the message with one of the chat's skills: the model gets the
// skill's instructions with this message, and the transcript shows the
// skill's name next to the text. An unknown skill is refused with the names
// the chat has. Text may then be empty.
Skill string `json:"skill,omitempty"`
// Agent hands the task in Text to one of your saved agents (its id or
// name). The chat must have the saved agents switched on.
Agent string `json:"agent,omitempty"`
}
// ChatsInvocation is the command a message was sent with.
type ChatsInvocation struct {
Kind string `json:"kind"` // skill or agent
Name string `json:"name"`
ID string `json:"id,omitempty"` // the saved agent's id
}
// ChatsContextResult says how a context command changed what the model sees:
// its size in tokens before and after.
type ChatsContextResult struct {
Before int `json:"before"`
After int `json:"after"`
}
// ChatsSendResponse says what happened to a message. With a turn running the
// message waits: Started is false and PendingID names it.
type ChatsSendResponse struct {
Started bool `json:"started"`
PendingID string `json:"pending_id,omitempty"`
}
// ChatsPending is a message that waits for the model's next request. Text is
// what you wrote; the files of the message are in Attachments.
type ChatsPending struct {
ID string `json:"id"`
Text string `json:"text"`
Attachments []ChatsAttachment `json:"attachments,omitempty"`
Seq int64 `json:"seq"`
}
// ChatsAttachment is a file attached to one of your messages. The model gets
// its path with the message; the transcript keeps it apart from the text.
type ChatsAttachment struct {
// Path is the file's path inside your home, for the files API
// (GET /api/chats/files/content?path=). It follows a rename of the chat's
// folder.
Path string `json:"path"`
Name string `json:"name"`
// Size is in bytes.
Size int64 `json:"size,omitempty"`
Mime string `json:"mime,omitempty"`
// Version is the version of the file that the message carried.
Version int64 `json:"version,omitempty"`
// Missing is true when the file is no longer in your files.
Missing bool `json:"missing,omitempty"`
}
// ChatsPendingEdit changes the text of a waiting message.
type ChatsPendingEdit struct {
Text string `json:"text"`
}
// ChatsMessage is one line of a conversation's transcript.
type ChatsMessage struct {
Seq int64 `json:"seq"`
Turn int `json:"turn"`
Role string `json:"role"` // user, assistant, tool, checkpoint, reasoning, compacted, cleared or goal
// Text is what was written. For a user line it is the text you typed:
// the files you attached are in Attachments.
Text string `json:"text"`
// Attachments are the files of a user line.
Attachments []ChatsAttachment `json:"attachments,omitempty"`
// DurMs is how long the model thought, for reasoning lines.
DurMs int64 `json:"dur_ms,omitempty"`
// App is set on a tool line whose tool has an MCP App: the interactive page
// that goes with the result.
App *ChatsToolApp `json:"app,omitempty"`
// Card is set on a tool line that points at a record, such as a trigger
// proposal (kind trigger_proposal, id the proposal's id).
Card *ChatsToolCard `json:"card,omitempty"`
// Loop is set on a user line that a loop of the chat sent by itself.
Loop *ChatsLoopMark `json:"loop,omitempty"`
// Rev is the change set of a checkpoint line, whose text lists the paths
// it changed inside your home, one a line. POST /api/chats/files/undo
// {rev} undoes it.
Rev int64 `json:"rev,omitempty"`
// Invoked is the skill or saved agent a user line was sent with.
Invoked *ChatsInvocation `json:"invoked,omitempty"`
// Before and After are the size of the model's context, in tokens, on a
// compacted line.
Before int `json:"before,omitempty"`
After int `json:"after,omitempty"`
// GoalEvent is set on a goal line: set, replaced, continue (a message the
// server sent by itself to go on; Text is that message and GoalTurn its
// automatic turn), paused (Text is the reason), resumed, done (Text is the
// summary), blocked (Text is the reason) or cleared.
GoalEvent string `json:"goal_event,omitempty"`
GoalTurn int64 `json:"goal_turn,omitempty"`
At time.Time `json:"at"`
}
// ChatsToolCard points the interface at a record that belongs to a tool result.
type ChatsToolCard struct {
Kind string `json:"kind"`
ID string `json:"id"`
// Files are the files in your home that a command (bash) created,
// changed or deleted, on the card of that call. Paths are inside your
// home, which is "/". FilesMore counts the files the card does not list.
Files []ChatsToolFile `json:"files,omitempty"`
FilesMore int `json:"files_more,omitempty"`
// FileKind is the kind, "deliverable" or "working", of the file that a
// files_change card names in its id (see ChatsChatFile.Kind).
FileKind string `json:"file_kind,omitempty"`
}
// ChatsToolFile is one file that a tool call changed.
type ChatsToolFile struct {
Path string `json:"path"`
// Rev is the change set that last touched the file in this call.
Rev int64 `json:"rev"`
// Op is created, changed or deleted.
Op string `json:"op"`
// Kind is "deliverable" or "working" (see ChatsChatFile.Kind).
Kind string `json:"kind,omitempty"`
}
// ChatsToolApp tells the interface which MCP App belongs to a tool result. The
// page itself and the calls of the page go through the two mcp-apps routes of
// the chat. In a transcript line it carries the arguments and the full result
// of the call, for the page to show; in a live event it is the reference only.
type ChatsToolApp struct {
// Server is "connector:<id>" for one of your connectors, or the name of an
// MCP server of the model.
Server string `json:"server"`
// Tool is the name of the tool on that server.
Tool string `json:"tool"`
// ResourceURI is the ui:// resource that holds the page.
ResourceURI string `json:"resource_uri"`
// Visibility is who may call the tool: "model", "app" or both.
Visibility []string `json:"visibility"`
// Input is the arguments of the call, at most 16 KiB.
Input json.RawMessage `json:"input,omitempty" swaggertype:"object"`
// Result is the full MCP result of the call, at most 256 KiB.
Result json.RawMessage `json:"result,omitempty" swaggertype:"object"`
// Cut says that the input or the result was too large and is left out.
Cut bool `json:"cut,omitempty"`
}
// ChatsMCPAppResource is the page of an MCP App, read from its server.
type ChatsMCPAppResource struct {
URI string `json:"uri"`
MimeType string `json:"mime_type"`
// HTML is the page. It must run in a sandboxed frame with no access to the
// origin of the interface.
HTML string `json:"html"`
// Meta is the ui metadata of the resource as the MCP Apps specification
// names it, cleaned: csp (connectDomains, resourceDomains, frameDomains and
// baseUriDomains hold only https or wss origins), permissions and prefersBorder.
Meta map[string]any `json:"meta"`
}
// ChatsMCPAppCall asks to run a tool of an MCP server for its app.
type ChatsMCPAppCall struct {
// Server is "connector:<id>" or the name of an MCP server of the chat's model.
Server string `json:"server"`
// Tool is a tool of that server whose visibility includes "app".
Tool string `json:"tool"`
// Arguments is a JSON object, at most 16 KiB.
Arguments json.RawMessage `json:"arguments,omitempty" swaggertype:"object"`
}
// ChatsMCPAppCallResult is the MCP result of the tool call: content,
// structuredContent and isError as the MCP specification names them.
type ChatsMCPAppCallResult struct {
Result json.RawMessage `json:"result" swaggertype:"object"`
}
// ChatsMessageList is the response of the transcript endpoint.
type ChatsMessageList struct {
Messages []ChatsMessage `json:"messages"`
}
// ChatsEvent is one server-sent event of a conversation. Kind is text,
// reasoning, tool_start, tool_end, checkpoint, turn_start, turn_end, error,
// pending_delivered, sub_agent_started, sub_agent_done, compacted, usage or
// trigger_proposed.
type ChatsEvent struct {
Kind string `json:"kind"`
Turn int `json:"turn,omitempty"`
Text string `json:"text,omitempty"`
// Tool is set for tool_start and tool_end.
Tool *ChatsToolEvent `json:"tool,omitempty"`
// IDs names the messages of pending_delivered, the conversation of
// sub_agent_started and sub_agent_done, or the proposal of trigger_proposed.
IDs []string `json:"ids,omitempty"`
// Before and After are context sizes in tokens, for compacted.
Before int `json:"before,omitempty"`
After int `json:"after,omitempty"`
// Steps is the step limit that a paused turn reached (paused only).
Steps int `json:"steps,omitempty"`
// Usage is set on usage events: what one model request used.
Usage *ChatsUsage `json:"usage,omitempty"`
// Code, Model, Retry and Detail say why a turn failed (error only). Code
// is a stable name of the cause, such as model_backend_missing; Model names
// the model it is about; Retry is the seconds until a model in cooldown
// loads again; Detail is the error text without addresses and secrets.
Code string `json:"code,omitempty"`
Model string `json:"model,omitempty"`
Retry int `json:"retry,omitempty"`
Detail string `json:"detail,omitempty"`
}
// ChatsUsage is what one model request used. It feeds the context meter and
// the tokens-per-second figure.
type ChatsUsage struct {
// Used is the number of tokens in the context the request sent.
Used int `json:"used"`
// Limit is the context window the loop plans with, 0 when it is not known.
Limit int `json:"limit,omitempty"`
// Completion is the number of tokens the request generated.
Completion int `json:"completion"`
// TokensPerSecond is the generation speed, 0 when the backend gave no timing.
TokensPerSecond float64 `json:"tokens_per_second,omitempty"`
// Model is the model the request used.
Model string `json:"model,omitempty"`
// Node is the worker node that served the request. It is set only when
// the server exposes the node header (--expose-node-header).
Node string `json:"node,omitempty"`
// PromptMs is the time the backend took to read the prompt, in
// milliseconds, when the backend reports it.
PromptMs float64 `json:"prompt_ms,omitempty"`
}
// ChatsToolEvent describes a tool call.
type ChatsToolEvent struct {
Name string `json:"name"`
Summary string `json:"summary,omitempty"`
OK bool `json:"ok,omitempty"`
Output string `json:"output,omitempty"`
// Seq is the sequence number of the call's transcript line, on tool_end. A
// client that wants the app's input and result reads that one line with
// GET /api/chats/{id}/messages?after=<seq-1>&limit=1.
Seq int64 `json:"seq,omitempty"`
// App is the reference of the MCP App of the call, on tool_end. It has no
// input and no result: read the transcript line for them.
App *ChatsToolApp `json:"app,omitempty"`
// Card is the record the result points at, on tool_end; see ChatsMessage.
Card *ChatsToolCard `json:"card,omitempty"`
}
// ChatsFile is a file or folder of the user's workspace. Paths are inside the
// user's home folder, which is "/".
type ChatsFile struct {
Path string `json:"path"`
Name string `json:"name"`
Kind string `json:"kind"` // file or dir
Size int64 `json:"size"`
Version int64 `json:"version"`
Rev int64 `json:"rev"`
// ConflictOf is set when an upload kept both files: it is the path the
// caller asked for, and Path is where the new content went.
ConflictOf string `json:"conflict_of,omitempty"`
// Managed is set on a path LocalAI manages: "localai" for the LocalAI
// folder and the settings files in it, "chats" for the Chats folder.
Managed string `json:"managed,omitempty"`
// Modified is when the entry last changed. A folder listing sets it.
Modified *time.Time `json:"modified,omitempty"`
}
// ChatsFilePreview is what the server reads out of a file that a browser
// cannot show by itself. Kind says which of the lists is set: "table" (a
// spreadsheet, a CSV or a TSV file), "document" (a Word document) or "slides"
// (a PowerPoint presentation). The values are text: the formatting is not
// kept.
type ChatsFilePreview struct {
Kind string `json:"kind"`
Sheets []ChatsFilePreviewSheet `json:"sheets,omitempty"`
Blocks []ChatsFilePreviewBlock `json:"blocks,omitempty"`
Slides []ChatsFilePreviewSlide `json:"slides,omitempty"`
// Truncated is set when a document or presentation had more text than
// the preview keeps.
Truncated bool `json:"truncated,omitempty"`
}
// ChatsFilePreviewSheet is one sheet of a table: at most the first 200 rows and
// 30 columns. TotalRows and TotalCols count the whole sheet.
type ChatsFilePreviewSheet struct {
Name string `json:"name"`
Rows [][]string `json:"rows"`
TotalRows int `json:"total_rows"`
TotalCols int `json:"total_cols"`
}
// ChatsFilePreviewBlock is one paragraph or table of a document. Type is
// "heading" (Level 1 to 9), "item" (a list item, Level 0 and up), "paragraph"
// or "table" (Rows holds the cells; Text is the rows as tab-separated lines).
type ChatsFilePreviewBlock struct {
Type string `json:"type"`
Level int `json:"level,omitempty"`
Text string `json:"text"`
Rows [][]string `json:"rows,omitempty"`
}
// ChatsFilePreviewSlide is one slide: its title, its other text one line per
// paragraph, and its speaker notes.
type ChatsFilePreviewSlide struct {
N int `json:"n"`
Title string `json:"title,omitempty"`
Lines []ChatsFilePreviewLine `json:"lines,omitempty"`
Notes string `json:"notes,omitempty"`
}
// ChatsFilePreviewLine is a paragraph of a slide and its outline level (0 is
// the top).
type ChatsFilePreviewLine struct {
Text string `json:"text"`
Level int `json:"level,omitempty"`
}
// WorkspaceChange is one event of the change feed of the user's files. Paths
// are inside the user's home folder, without a leading slash. A "resync" event
// has the revision only: changes were left out, so read again what you show.
type WorkspaceChange struct {
// Revision is the newest revision of the changes; it is also the event id,
// which a client sends back (Last-Event-ID or ?after=) to resume.
Revision int64 `json:"revision"`
Paths []string `json:"paths,omitempty"`
// Kinds are the kinds of the change sets: user, agent, upload or undo.
Kinds []string `json:"kinds,omitempty"`
}
// ChatsFileList is a folder with its entries, sorted by name.
type ChatsFileList struct {
Path string `json:"path"`
Entries []ChatsFile `json:"entries"`
// MaxFileSize is the largest file an upload may send, in bytes. Zero or
// absent: the server reports no limit.
MaxFileSize int64 `json:"max_file_size,omitempty"`
}
// ChatsFolderRequest creates a folder.
type ChatsFolderRequest struct {
Path string `json:"path"`
}
// ChatsMoveRequest moves or renames a file or folder.
type ChatsMoveRequest struct {
From string `json:"from"`
To string `json:"to"`
}
// ChatsUndoRequest undoes one change set.
type ChatsUndoRequest struct {
Rev int64 `json:"rev"`
SkipConflicts bool `json:"skip_conflicts,omitempty"`
}
// ChatsFileConflict is something an undo could not revert.
type ChatsFileConflict struct {
Path string `json:"path"`
Reason string `json:"reason"`
}
// ChatsUndoResponse is the result of an undo.
type ChatsUndoResponse struct {
Rev int64 `json:"rev"`
Conflicts []ChatsFileConflict `json:"conflicts,omitempty"`
}
// ChatsTrashEntry is a deletion that can be undone.
type ChatsTrashEntry struct {
Rev int64 `json:"rev"`
Title string `json:"title"`
At time.Time `json:"at"`
Paths []string `json:"paths"`
// Files, Folders and Bytes say what deleting the entry for good removes.
Files int `json:"files"`
Folders int `json:"folders"`
Bytes int64 `json:"bytes"`
}
// ChatsTrashTotals counts what is in the Trash, or what a purge removed.
type ChatsTrashTotals struct {
Files int `json:"files"`
Folders int `json:"folders"`
Bytes int64 `json:"bytes"`
}
// ChatsTrash lists the deletions that can be undone, newest first.
type ChatsTrash struct {
Entries []ChatsTrashEntry `json:"entries"`
// Totals counts everything in the Trash, also what a long list leaves out.
// It is what "empty the Trash" removes.
Totals ChatsTrashTotals `json:"totals"`
}
// ChatsFileOp is one change of a change set.
type ChatsFileOp struct {
Kind string `json:"kind"` // create, write, move or delete
Path string `json:"path"`
From string `json:"from,omitempty"`
}
// ChatsHistoryEntry is one change set.
type ChatsHistoryEntry struct {
Rev int64 `json:"rev"`
Title string `json:"title"`
Kind string `json:"kind"` // user, agent, upload or undo
Chat string `json:"chat,omitempty"`
At time.Time `json:"at"`
Ops []ChatsFileOp `json:"ops"`
}
// ChatsHistory lists change sets, newest first.
type ChatsHistory struct {
Entries []ChatsHistoryEntry `json:"entries"`
}
// ChatsSearchHit is a file whose name matches a search, or a line of a file
// that contains the text.
type ChatsSearchHit struct {
Path string `json:"path"`
Kind string `json:"kind"`
Line int `json:"line,omitempty"`
Snippet string `json:"snippet,omitempty"`
// Document is true for a line of the text read from a PDF, Word, Excel or
// PowerPoint file. Line is then a line of that text.
Document bool `json:"document,omitempty"`
// Page, Sheet and Slide say where a document hit is, when the document has
// pages, sheets or slides.
Page int `json:"page,omitempty"`
Sheet string `json:"sheet,omitempty"`
Slide int `json:"slide,omitempty"`
}
// ChatsSearchResult lists name matches first, then lines of text files, then
// lines of documents.
type ChatsSearchResult struct {
Hits []ChatsSearchHit `json:"hits"`
// Indexing counts the documents below the path whose text is not read
// yet: the same search a moment later may find more.
Indexing int `json:"indexing,omitempty"`
// Limited is set when the search stopped at one of its limits before it
// looked at every file below the path: a file that matches may be
// missing, so narrow the path or the query.
Limited bool `json:"limited,omitempty"`
}
// ChatsSkill is a skill a chat can use.
type ChatsSkill struct {
// ID is "layer:name". A switch with the ID turns off the skill of that
// layer; a switch with the plain name turns off the skill that wins.
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
// Layer is where it comes from: folder (a .skills folder of your files),
// personal, shared, addon (an installed add-on ships it) or builtin.
Layer string `json:"layer"`
// Addon and AddonTitle name the add-on that provides a skill of the addon
// layer.
Addon string `json:"addon,omitempty"`
AddonTitle string `json:"addon_title,omitempty"`
// Path is the path of its SKILL.md as the model sees it.
Path string `json:"path"`
// Enabled is false when the chat has switched it off.
Enabled bool `json:"enabled"`
// Shadowed names the layer of a nearer skill with the same name, which wins.
Shadowed string `json:"shadowed,omitempty"`
// ShadowedBy is the add-on whose skill wins over this one, when the
// winner ships with an add-on (a built-in skill an add-on replaces, or
// the skill of an add-on that is not the chosen one).
ShadowedBy string `json:"shadowed_by,omitempty"`
// Fallback is set on a built-in skill that serves because the add-on
// skill that replaces it is off or not running in the chat: the title of
// that add-on.
Fallback string `json:"fallback,omitempty"`
// Invalid says why a SKILL.md was skipped.
Invalid string `json:"invalid,omitempty"`
// Editable is true when you may change or delete it here.
Editable bool `json:"editable"`
}
// ChatsSkillList is the skills a user has.
type ChatsSkillList struct {
Skills []ChatsSkill `json:"skills"`
// Notes say what could not be read or searched.
Notes []string `json:"notes,omitempty"`
}
// ChatsSkillWrite creates a skill.
type ChatsSkillWrite struct {
Layer string `json:"layer"` // personal or shared
Name string `json:"name"`
Description string `json:"description"`
Body string `json:"body"`
}
// ChatsSkillUpdate replaces a skill's description and body.
type ChatsSkillUpdate struct {
Description string `json:"description"`
Body string `json:"body"`
}
// ChatsSkillDetail is one skill with its text.
type ChatsSkillDetail struct {
Name string `json:"name"`
Layer string `json:"layer"`
Description string `json:"description"`
Body string `json:"body"`
Editable bool `json:"editable"`
}
// ChatsSkillSwitches sets the skills a chat has switched off.
type ChatsSkillSwitches struct {
Off []string `json:"off"`
}
// ChatsAddonSwitches sets the add-ons whose tools a chat has switched off.
type ChatsAddonSwitches struct {
Off []string `json:"off"`
}
// ChatsAddon is one add-on whose tools the user may use, with the chat's switch.
type ChatsAddon struct {
ID string `json:"id"`
Tools int `json:"tools"`
Off bool `json:"off"`
}
// ChatsAddonList lists the add-ons a chat can use.
type ChatsAddonList struct {
Addons []ChatsAddon `json:"addons"`
// Commands says whether the chat can run commands and code at its next
// turn: available, off (the add-on that runs them is switched off for this
// chat or not named by its agent), unavailable (installed, but it cannot run
// them on this server), none (no add-on that runs them is installed for
// the user) or switched_off (the chat switched the shell group off, so
// nothing runs them whoever would serve them).
Commands string `json:"commands"`
// CommandsReason is why commands cannot run, when Commands is unavailable.
CommandsReason string `json:"commands_reason,omitempty"`
// CommandsIsolation is isolated (a sandbox) or host (directly on this
// machine, with its files and network), when Commands is available.
CommandsIsolation string `json:"commands_isolation,omitempty"`
// CommandsProvider serves bash, when Commands is available: builtin (the
// built-in bash on this machine) or the ID of the add-on.
CommandsProvider string `json:"commands_provider,omitempty"`
// CommandsConsent is ask when each command of the built-in bash waits for
// a yes in the chat.
CommandsConsent string `json:"commands_consent,omitempty"`
// CommandsFallback is the add-on that would serve bash but does not (it is
// off for this chat, or cannot run), when the built-in serves instead;
// CommandsFallbackReason says why: off, or the add-on's reason.
CommandsFallback string `json:"commands_fallback,omitempty"`
CommandsFallbackReason string `json:"commands_fallback_reason,omitempty"`
}
// ChatsHostExecConsent answers the card that asks to run one command on this
// machine: decision is once (administrators only) or deny.
type ChatsHostExecConsent struct {
ID string `json:"id"`
Decision string `json:"decision" enums:"once,deny"`
}
// ChatsHostExecEvent is one command the built-in bash ran on this machine.
type ChatsHostExecEvent struct {
User string `json:"user"`
UserName string `json:"user_name,omitempty"`
Chat string `json:"chat"`
Command string `json:"command"`
Exit int `json:"exit"`
DurationMS int64 `json:"duration_ms"`
TimedOut bool `json:"timed_out,omitempty"`
At time.Time `json:"at"`
}
// ChatsHostExecEventList is the audit of the commands, newest first.
type ChatsHostExecEventList struct {
Events []ChatsHostExecEvent `json:"events"`
}
// ChatsBuiltinTool is one built-in chat tool in Settings > Tools.
type ChatsBuiltinTool struct {
ID string `json:"id"`
Label string `json:"label"`
Description string `json:"description"`
// Group is the per-chat switch group (web), or empty.
Group string `json:"group,omitempty"`
// Setting is the runtime setting that switches it off; Options are the
// other runtime settings that configure it.
Setting string `json:"setting"`
Options []string `json:"options"`
Enabled bool `json:"enabled"`
// ReadOnly says the tool only reads; Replaceable that an add-on tool may
// replace it by declaring shadows with its name.
ReadOnly bool `json:"read_only"`
Replaceable bool `json:"replaceable"`
// Provider serves it now: builtin, an add-on's ID, or empty for nothing.
Provider string `json:"provider"`
// Candidates are the add-ons that replace it; Pinned the one chosen.
// Candidates come oldest install first; Default is the first, the one
// that serves with no pin ("installed first"). InstalledAt maps a
// candidate to its install time (RFC 3339, UTC) when known.
Candidates []string `json:"candidates"`
Default string `json:"default,omitempty"`
Pinned string `json:"pinned,omitempty"`
InstalledAt map[string]string `json:"installed_at,omitempty"`
// Titles maps a candidate to its title. Stopped lists the candidates that
// are installed but stopped or disabled: they can be chosen, and serve
// once they run.
Titles map[string]string `json:"titles,omitempty"`
Stopped []string `json:"stopped,omitempty"`
// Note is "not running" when an installed add-on replaces the tool but
// none serves now, so the built-in serves (or nothing, when it is off).
Note string `json:"note,omitempty"`
// Mode is chats_host_exec as it applies (bash only): off, ask or on.
Mode string `json:"mode,omitempty"`
// Unavailable says why this server cannot run the built-in, or is empty.
Unavailable string `json:"unavailable,omitempty"`
}
// ChatsBuiltinSkill is one built-in skill that installed add-ons replace, in
// Settings > Tools.
type ChatsBuiltinSkill struct {
ID string `json:"id"`
// Provider serves it now: builtin or an add-on's ID.
Provider string `json:"provider"`
// Candidates are the add-ons that replace it, by ID, and Titles their
// titles; Pinned the one chosen in chats_tool_providers.
// Candidates come oldest install first; Default is the first, the one
// that serves with no pin. InstalledAt maps a candidate to its install
// time (RFC 3339, UTC) when known.
Candidates []string `json:"candidates"`
Default string `json:"default,omitempty"`
Titles map[string]string `json:"titles,omitempty"`
Pinned string `json:"pinned,omitempty"`
InstalledAt map[string]string `json:"installed_at,omitempty"`
// Note says why the built-in serves although an add-on replaces it, or is
// empty: the chosen add-on is not running.
Note string `json:"note,omitempty"`
}
// ChatsBuiltinToolList lists the built-in chat tools, and the built-in skills
// that at least one installed add-on replaces.
type ChatsBuiltinToolList struct {
Tools []ChatsBuiltinTool `json:"tools"`
Skills []ChatsBuiltinSkill `json:"skills"`
}
// ChatsNotification is one item of the inbox. Kind is run_ended, run_failed or
// needs_attention; Status is ok or failed. Source is what sent it: trigger,
// connector or system (empty on items stored before sources existed). Link is
// the in-app path that opens what it is about.
type ChatsNotification struct {
ID int64 `json:"id"`
Kind string `json:"kind"`
Status string `json:"status"`
Source string `json:"source,omitempty"`
Title string `json:"title"`
Body string `json:"body"`
Link string `json:"link"`
Read bool `json:"read"`
Created time.Time `json:"created"`
}
// ChatsNotificationList is the response of the inbox list endpoint.
type ChatsNotificationList struct {
Notifications []ChatsNotification `json:"notifications"`
Unread int `json:"unread"`
// Total is how many notifications the user keeps, more than the list when a limit cut it.
Total int `json:"total"`
}
// ChatsNotificationCount is the number of unread notifications. After a
// delete, Deleted is how many it removed.
type ChatsNotificationCount struct {
Unread int `json:"unread"`
Deleted int `json:"deleted,omitempty"`
}
// ChatsNotificationsRead marks notifications read: the named ones, or all.
type ChatsNotificationsRead struct {
IDs []int64 `json:"ids,omitempty"`
All bool `json:"all,omitempty"`
}
// ChatsNotificationsDelete names the notifications to delete: the ids, or all
// of them. With all, read_only keeps the unread ones. Ids and all together are
// refused.
type ChatsNotificationsDelete struct {
IDs []int64 `json:"ids,omitempty"`
All bool `json:"all,omitempty"`
ReadOnly bool `json:"read_only,omitempty"`
}
// ChatsTrigger is a trigger: work that starts by itself, as you, in a new chat.
// Kind is schedule or file. A schedule trigger has schedule and time_zone; a
// file trigger has folder, a path inside your files, and quiet_seconds.
type ChatsTrigger struct {
ID string `json:"id"`
Name string `json:"name"`
Prompt string `json:"prompt"`
Kind string `json:"kind"`
Schedule string `json:"schedule,omitempty"`
TimeZone string `json:"time_zone,omitempty"`
Folder string `json:"folder,omitempty"`
QuietSeconds int `json:"quiet_seconds,omitempty"`
DailyCap int `json:"daily_cap"`
Enabled bool `json:"enabled"`
AgentID string `json:"agent_id,omitempty"`
Model string `json:"model,omitempty"`
SourceChatID string `json:"source_chat_id,omitempty"`
// Summary says when it runs in plain words. NextRun is set for a schedule
// trigger that is on.
Summary string `json:"summary"`
NextRun *time.Time `json:"next_run,omitempty"`
// LastRun is the newest run, if any, and LastDone the end of the newest
// run that finished without an error.
LastRun *ChatsTriggerRun `json:"last_run,omitempty"`
LastDone *time.Time `json:"last_done,omitempty"`
// MinGapSeconds is the shortest time between two runs of a file trigger.
MinGapSeconds int `json:"min_gap_seconds,omitempty"`
// Plan says what a run reads and writes and which Home widget shows the
// file it writes; Cost what a run costs.
Plan ChatsTriggerPlan `json:"plan"`
Cost ChatsTriggerCost `json:"cost"`
}
// ChatsTriggerProposal is a trigger a chat proposed and what became of it.
// State is pending (it waits), accepted (trigger is the trigger that accepting
// made, the same id), declined or undone (trigger is the record kept so the
// chat can show a receipt and offer to restore it). CanUndo is set on an
// accepted trigger that has not run and was not edited; UndoBlocked says why
// not: ran or edited.
type ChatsTriggerProposal struct {
State string `json:"state"`
Trigger ChatsTrigger `json:"trigger"`
AnsweredAt *time.Time `json:"answered_at,omitempty"`
CanUndo bool `json:"can_undo"`
UndoBlocked string `json:"undo_blocked,omitempty"`
}
// ChatsTriggerChange is a change of a trigger that a model asked for in a
// chat, and what became of it. Op is update, delete, pause or resume. State is
// pending (an update or a delete waits for Apply), applied, dismissed or
// undone; a pause or a resume is applied when it is asked for. Before and
// After are the trigger before and after the change (After is absent for a
// delete), and Fields the fields that differ. Stale is set on a pending change
// whose trigger was changed or removed since, so it can no longer apply.
// CanUndo is set on an applied change that can still be undone (for a day);
// UndoBlocked says why not: expired, edited, removed or full.
type ChatsTriggerChange struct {
ID string `json:"id"`
Op string `json:"op"`
State string `json:"state"`
TriggerID string `json:"trigger_id"`
Before ChatsTrigger `json:"before"`
After *ChatsTrigger `json:"after,omitempty"`
Fields []string `json:"fields"`
AnsweredAt *time.Time `json:"answered_at,omitempty"`
Stale bool `json:"stale"`
CanUndo bool `json:"can_undo"`
UndoBlocked string `json:"undo_blocked,omitempty"`
LastRun *ChatsTriggerRun `json:"last_run,omitempty"`
}
// ChatsTriggerPlan is what a run of a trigger does, as the model that proposed
// it described it. Every field is optional.
type ChatsTriggerPlan struct {
Reads string `json:"reads,omitempty"`
// Writes is a path in the owner's home, such as data/inbox.json.
Writes string `json:"writes,omitempty"`
// Widget is the id of the Home widget in home.json that shows Writes.
Widget string `json:"widget,omitempty"`
}
// ChatsTriggerCost is what the runs of a trigger cost, in tokens. LocalAI keeps
// no price list, so there is no price.
type ChatsTriggerCost struct {
// EstimatedTokens is the estimate of the proposal; 0 is unknown.
EstimatedTokens int `json:"estimated_tokens,omitempty"`
// AverageTokens is the mean of the last finished runs that reported their
// tokens, and Runs how many runs that is.
AverageTokens int `json:"average_tokens,omitempty"`
Runs int `json:"runs,omitempty"`
// MaxRunsPerDay is the most runs one day can have: the daily limit, or
// fewer when the schedule runs less often.
MaxRunsPerDay int `json:"max_runs_per_day"`
// Remote is true when the model runs on a remote service, so the tokens
// leave this server.
Remote bool `json:"remote"`
}
// ChatsTriggerWrite creates or replaces a trigger.
type ChatsTriggerWrite struct {
Name string `json:"name"`
Prompt string `json:"prompt"`
Kind string `json:"kind"`
Schedule string `json:"schedule,omitempty"`
TimeZone string `json:"time_zone,omitempty"`
Folder string `json:"folder,omitempty"`
QuietSeconds int `json:"quiet_seconds,omitempty"`
DailyCap int `json:"daily_cap,omitempty"`
Enabled bool `json:"enabled"`
// AgentID is optional. Model is needed when neither the agent nor the source
// chat names one.
AgentID string `json:"agent_id,omitempty"`
Model string `json:"model,omitempty"`
SourceChatID string `json:"source_chat_id,omitempty"`
// MinGapSeconds is the shortest time between two runs of a file trigger;
// 0 keeps the trigger's value (one minute for a new one).
MinGapSeconds int `json:"min_gap_seconds,omitempty"`
// Plan describes the runs. An empty field keeps the trigger's value.
Plan *ChatsTriggerPlan `json:"plan,omitempty"`
// EstimatedTokens is about how many tokens one run uses; 0 keeps the
// trigger's value.
EstimatedTokens int `json:"estimated_tokens,omitempty"`
}
// ChatsTriggerList is the response of the trigger list endpoint.
type ChatsTriggerList struct {
Triggers []ChatsTrigger `json:"triggers"`
}
// ChatsTriggerRun is one start of a trigger. Status is queued, running, done,
// failed or skipped; reason says why for the last two.
type ChatsTriggerRun struct {
ID string `json:"id"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
ChatID string `json:"chat_id,omitempty"`
File string `json:"file,omitempty"`
Due time.Time `json:"due"`
Started *time.Time `json:"started,omitempty"`
Ended *time.Time `json:"ended,omitempty"`
// Tokens is what the run's model requests used, when it is known.
Tokens int `json:"tokens,omitempty"`
}
// ChatsTriggerRunList is the response of the runs endpoint.
type ChatsTriggerRunList struct {
Runs []ChatsTriggerRun `json:"runs"`
}
// ChatsTriggerPreview lists the next times a schedule runs.
type ChatsTriggerPreview struct {
Times []time.Time `json:"times"`
Summary string `json:"summary"`
}
// ChatsTriggerConfirm confirms a proposal. TimeZone is used when the proposal
// names none.
type ChatsTriggerConfirm struct {
TimeZone string `json:"time_zone,omitempty"`
}
// ChatsPushKey is the VAPID public key a browser needs to subscribe to push.
type ChatsPushKey struct {
PublicKey string `json:"public_key"`
}
// ChatsPushSubscription is a browser's PushSubscription as it serialises to JSON.
type ChatsPushSubscription struct {
Endpoint string `json:"endpoint"`
Keys struct {
P256dh string `json:"p256dh"`
Auth string `json:"auth"`
} `json:"keys"`
}
// ChatsPushUnsubscribe names the subscription to remove.
type ChatsPushUnsubscribe struct {
Endpoint string `json:"endpoint"`
}
// ChatsConnector is a connector binding: a bot bound to one of your saved agents.
// The bot token is never returned; has_token says whether one is stored.
type ChatsConnector struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
Addon string `json:"addon"`
AgentID string `json:"agent_id"`
Enabled bool `json:"enabled"`
AcceptAny bool `json:"accept_any"`
Allow []string `json:"allow"`
NotifyChat string `json:"notify_chat,omitempty"`
HasToken bool `json:"has_token"`
Created time.Time `json:"created"`
// ReplyMode is what a sender sees while a turn runs: off (typing, then
// the answer), progress (a working message that names the tools as they
// start) or stream (a working message that shows the answer as it is
// written). The answer replaces the working message.
ReplyMode string `json:"reply_mode"`
// Conversation says which chat a message belongs to: sender (one chat
// per sender and per platform thread), chat (one chat per group, channel
// or direct chat), window (sender, and a new chat after window_minutes
// without a message) or thread (a new chat per message; a reply to an
// answer goes on in its chat).
Conversation string `json:"conversation"`
WindowMinutes int `json:"window_minutes,omitempty"`
// SendFiles sends the files that a turn makes to the sender, after the
// answer.
SendFiles bool `json:"send_files"`
// Active is false when the add-on that serves the binding is not running. The
// binding is kept and works once the add-on is installed.
Active bool `json:"active"`
// Reason says in plain words why the binding is not active.
Reason string `json:"reason,omitempty"`
}
// ChatsConnectorKind is a kind of binding that an installed connector add-on serves.
type ChatsConnectorKind struct {
Addon string `json:"addon"`
Kind string `json:"kind"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
SenderHint string `json:"sender_hint,omitempty"`
// Page is the add-on page that adds and edits bindings of this kind.
Page string `json:"page,omitempty"`
}
// ChatsConnectorKindList lists the connector kinds of the add-ons that run now.
type ChatsConnectorKindList struct {
Kinds []ChatsConnectorKind `json:"kinds"`
}
// ChatsConnectorWrite creates or replaces a connector binding. token is needed to
// create one; an update without it keeps the stored token.
type ChatsConnectorWrite struct {
// Kind is the platform, such as telegram. It is used to create a binding and
// ignored by an update. Empty means telegram.
Kind string `json:"kind,omitempty"`
Name string `json:"name"`
Token string `json:"token,omitempty"`
AgentID string `json:"agent_id"`
Enabled bool `json:"enabled"`
AcceptAny bool `json:"accept_any"`
// ConfirmAcceptAny must be the exact text "accept any message" in the
// request that turns accept_any on (a create with accept_any, or an update
// that changes it from off to on). Anyone who can message the bot then uses
// the agent's tools as you, so the server asks you to say you mean it.
ConfirmAcceptAny string `json:"confirm_accept_any,omitempty"`
Allow []string `json:"allow,omitempty"`
NotifyChat string `json:"notify_chat,omitempty"`
// ReplyMode is off, progress or stream; empty means progress.
ReplyMode string `json:"reply_mode,omitempty"`
// Conversation is sender, chat, window or thread; empty means sender.
Conversation string `json:"conversation,omitempty"`
// WindowMinutes is the time window of the window mode, 1 to 10080.
WindowMinutes int `json:"window_minutes,omitempty"`
// SendFiles turns the sending of a turn's files on or off. Absent means
// on for a new binding and unchanged for an update.
SendFiles *bool `json:"send_files,omitempty"`
}
// ChatsConnectorList is the response of the connector list endpoint.
type ChatsConnectorList struct {
Connectors []ChatsConnector `json:"connectors"`
}
// ChatsConnectorTest is the bot that a token belongs to.
type ChatsConnectorTest struct {
BotID string `json:"bot_id"`
BotUsername string `json:"bot_username"`
}
// ChatsConnectorSender is a person who messaged a bot.
type ChatsConnectorSender struct {
ID string `json:"id"`
Name string `json:"name"`
Count int `json:"count"`
First time.Time `json:"first"`
Last time.Time `json:"last"`
Allowed bool `json:"allowed"`
}
// ChatsConnectorSenderList is the sender log of a binding, newest first.
type ChatsConnectorSenderList struct {
Senders []ChatsConnectorSender `json:"senders"`
}
// ChatsConversionItem is one thing the conversion of the old data handled.
type ChatsConversionItem struct {
// Kind is agent, trigger, skill, chat or connector.
Kind string `json:"kind"`
Name string `json:"name"`
// Status is converted, skipped, failed or dropped.
Status string `json:"status"`
// Reason names what was dropped or what went wrong. It holds no secret.
Reason string `json:"reason,omitempty"`
}
// ChatsConversionTotals counts the items of a report by status.
type ChatsConversionTotals struct {
Converted int `json:"converted"`
Skipped int `json:"skipped"`
Failed int `json:"failed"`
Dropped int `json:"dropped"`
}
// ChatsConversion is the report of the conversion of the old chat, agents,
// triggers and skills, for one user. An administrator also gets the totals of
// every user.
type ChatsConversion struct {
// Done says a conversion ran at least once.
Done bool `json:"done"`
At *time.Time `json:"at,omitempty"`
Counts ChatsConversionTotals `json:"counts"`
Items []ChatsConversionItem `json:"items"`
// Totals is set for an administrator: the counts of all users together.
Totals *ChatsConversionTotals `json:"totals,omitempty"`
}
// ChatsBrowserImport is the outcome of an upload of the chats the old chat page
// kept in the browser.
type ChatsBrowserImport struct {
// Imported chats are new. Skipped ones were there already, or have no
// creation date or no message to import. Failed ones could not be stored.
Imported int `json:"imported"`
Skipped int `json:"skipped"`
Failed int `json:"failed"`
}
// ChatsMCPConnector is an MCP server that you defined, for your chats. Its token,
// headers and environment are never returned: the has_ fields say what is stored.
type ChatsMCPConnector struct {
ID string `json:"id"`
Name string `json:"name"`
Transport string `json:"transport"` // http or stdio
URL string `json:"url,omitempty"`
Command string `json:"command,omitempty"`
Args []string `json:"args"`
HasToken bool `json:"has_token"`
HasHeaders bool `json:"has_headers"`
HasEnv bool `json:"has_env"`
Enabled bool `json:"enabled"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// ChatsMCPConnectorWrite creates or replaces an MCP connector. token, headers and
// env are write-only: left out they keep the stored value, and an empty value
// clears it.
type ChatsMCPConnectorWrite struct {
Name string `json:"name"`
Transport string `json:"transport"`
URL string `json:"url,omitempty"`
Command string `json:"command,omitempty"`
Args []string `json:"args,omitempty"`
Token *string `json:"token,omitempty"`
Headers *map[string]string `json:"headers,omitempty"`
Env *map[string]string `json:"env,omitempty"`
Enabled bool `json:"enabled"`
}
// ChatsMCPConnectorList is the response of the MCP connector list endpoint.
type ChatsMCPConnectorList struct {
Connectors []ChatsMCPConnector `json:"connectors"`
}
// ChatsMCPConnectorTest is the outcome of a connection test: the names of the
// tools the server offers, or why it could not be used.
type ChatsMCPConnectorTest struct {
OK bool `json:"ok"`
Tools []string `json:"tools"`
// ToolDetails is each tool with the description its server gives, in the
// order of tools.
ToolDetails []ChatsMCPConnectorTool `json:"tool_details"`
Error string `json:"error,omitempty"`
}
// ChatsMCPConnectorTool is one tool of an MCP connector.
type ChatsMCPConnectorTool struct {
Name string `json:"name" example:"search"`
Description string `json:"description,omitempty" example:"Search the documentation."`
}
// ChatsMCPSwitches sets the MCP servers and connectors a chat has switched off:
// the name of a server of the model, or "connector:<id>" for a connector.
type ChatsMCPSwitches struct {
Off []string `json:"off"`
}
// ChatsMCPServerSwitch is a server of the model with the chat's switch.
type ChatsMCPServerSwitch struct {
Name string `json:"name"`
Off bool `json:"off"`
Usable bool `json:"usable"`
Reason string `json:"reason,omitempty"`
}
// ChatsMCPConnectorSwitch is one of your connectors with the chat's switch.
type ChatsMCPConnectorSwitch struct {
ID string `json:"id"`
Name string `json:"name"`
Off bool `json:"off"`
Usable bool `json:"usable"`
Reason string `json:"reason,omitempty"`
}
// ChatsBuiltinGroup is a group of built-in tools of a chat, such as the web
// tools, with the chat's switch. Only the tools an administrator has on are
// listed; a group with none is absent.
type ChatsBuiltinGroup struct {
Group string `json:"group" example:"web"`
Tools []string `json:"tools" example:"web_search,web_fetch"`
Off bool `json:"off"`
// OptIn says that the group is off until the chat switches it on with
// "on" (saved_agents).
OptIn bool `json:"opt_in,omitempty"`
// Usable is false when the group cannot serve this chat now; Reason says
// why.
Usable bool `json:"usable"`
Reason string `json:"reason,omitempty"`
// Provider is "builtin" when LocalAI serves the tools itself, the ID of
// the add-on that serves them in this chat when one shadows them, or
// "mixed" when the tools have different providers.
Provider string `json:"provider" example:"builtin"`
// Providers says who serves each tool, by tool name.
Providers map[string]string `json:"providers" example:"web_search:builtin"`
}
// ChatsWebTestRequest is the body of a test web search.
type ChatsWebTestRequest struct {
Query string `json:"query" example:"LocalAI"`
}
// ChatsWebResult is one result of a web search.
type ChatsWebResult struct {
Title string `json:"title"`
URL string `json:"url"`
Snippet string `json:"snippet"`
}
// ChatsWebTestResponse is the answer of a test web search: the provider and
// its first results, or the error a user would read.
type ChatsWebTestResponse struct {
OK bool `json:"ok"`
Provider string `json:"provider" example:"duckduckgo"`
Host string `json:"host" example:"duckduckgo.com"`
Results []ChatsWebResult `json:"results"`
Error string `json:"error,omitempty"`
// Providers says what each provider of the order did with the query, in
// order: every provider is asked, so the administrator sees which work.
Providers []ChatsWebProviderTest `json:"providers"`
}
// ChatsWebProviderTest is what one search provider did with the test query.
type ChatsWebProviderTest struct {
Name string `json:"name" example:"duckduckgo"`
Host string `json:"host,omitempty" example:"duckduckgo.com"`
// Outcome is ok, no_results, challenged (a bot check), blocked (403 or
// 429), timeout or error.
Outcome string `json:"outcome" example:"challenged"`
Results int `json:"results"`
Reason string `json:"reason,omitempty" example:"challenged by a bot check"`
}
// ChatsBuiltinSwitches is the body that sets the groups of built-in tools a
// chat has switched off.
type ChatsBuiltinSwitches struct {
Off []string `json:"off"`
// On lists the opt-in groups the chat switches on (saved_agents). Omit
// it to keep them as they are.
On *[]string `json:"on,omitempty"`
}
// ChatsToolsPanel lists what a chat can use besides its own tools: the groups
// of built-in tools, add-ons, the MCP servers of its model and your
// connectors, each with the chat's switch.
type ChatsToolsPanel struct {
Builtins []ChatsBuiltinGroup `json:"builtins"`
Addons []ChatsAddon `json:"addons"`
Servers []ChatsMCPServerSwitch `json:"servers"`
Connectors []ChatsMCPConnectorSwitch `json:"connectors"`
// Off is the list of MCP switches in force.
Off []string `json:"off"`
// Fallbacks are the built-in tools that an installed add-on replaces but
// that no add-on serves in this chat now: the add-on is stopped, off for
// the chat, not granted by its agent, or cannot run.
Fallbacks []ChatsToolFallback `json:"fallbacks,omitempty"`
}
// ChatsToolFallback is one built-in tool whose replacing add-on cannot serve
// the chat now.
type ChatsToolFallback struct {
// Tool is the built-in tool, such as bash.
Tool string `json:"tool" example:"bash"`
// Addon and AddonTitle name the add-on that would serve it: the one an
// administrator chose, or the one installed first.
Addon string `json:"addon" example:"sandbox"`
AddonTitle string `json:"addon_title" example:"Sandbox"`
// Builtin says that the built-in tool serves instead; false when the
// built-in is off too, so the chat has no such tool.
Builtin bool `json:"builtin"`
}
// ChatsAdminAgent is a saved agent as an administrator sees it: with its owner,
// and without the instructions, which are the owner's text.
type ChatsAdminAgent struct {
ID string `json:"id"`
Owner string `json:"owner"`
Name string `json:"name"`
Description string `json:"description"`
Model string `json:"model,omitempty"`
MCPServers []string `json:"mcp_servers"`
Addons []string `json:"addons"`
Collections []string `json:"collections"`
Skills []string `json:"skills"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
// ChatsAdminAgentList is the response of the administrator's agents listing.
type ChatsAdminAgentList struct {
Agents []ChatsAdminAgent `json:"agents"`
}
// ChatsAdminTrigger is a trigger with its owner.
type ChatsAdminTrigger struct {
Owner string `json:"owner"`
ChatsTrigger
}
// ChatsAdminTriggerList is the response of the administrator's triggers listing.
type ChatsAdminTriggerList struct {
Triggers []ChatsAdminTrigger `json:"triggers"`
}
// ChatsAdminConnector is a connector binding with its owner. It never has the token.
type ChatsAdminConnector struct {
Owner string `json:"owner"`
ChatsConnector
}
// ChatsAdminConnectorList is the response of the administrator's connectors listing.
type ChatsAdminConnectorList struct {
Connectors []ChatsAdminConnector `json:"connectors"`
}
// ChatsWellKnownFile is a well-known file of the home (persona.json,
// user.json, shortcuts.json, home.json, ui.json, or the data file of a Home
// widget), validated against its JSON Schema.
type ChatsWellKnownFile struct {
Name string `json:"name"`
Path string `json:"path"`
Exists bool `json:"exists"`
// Version is what a write sends back in If-Match. It is 0 when the file
// does not exist.
Version int64 `json:"version"`
Rev int64 `json:"rev"`
// Data is the file when it is valid, its last valid version when it is
// not (fallback is then true), or an empty object.
Data map[string]any `json:"data"`
// Valid is false when the file breaks its schema; errors says why.
Valid bool `json:"valid"`
Errors []string `json:"errors,omitempty"`
Fallback bool `json:"fallback,omitempty"`
FallbackRev int64 `json:"fallback_rev,omitempty"`
// Prefilled names the keys of data that do not come from the file, and
// where they come from ("account").
Prefilled map[string]string `json:"prefilled,omitempty"`
}
// ChatsWellKnownWrite changes a well-known file. The keys of values replace
// the keys of the file and a null removes one; the other keys of the file
// stay. With replace, values is the whole new file.
type ChatsWellKnownWrite struct {
Values map[string]any `json:"values"`
Replace bool `json:"replace,omitempty"`
}
// ChatsWellKnownRevert names the version a revert writes back. Without rev,
// the previous valid version.
type ChatsWellKnownRevert struct {
Rev int64 `json:"rev,omitempty"`
}
// ChatsWellKnownVersion is one content a well-known file had.
type ChatsWellKnownVersion struct {
Rev int64 `json:"rev"`
At time.Time `json:"at"`
// Kind says who made it: user (you, in a form or the Files page), agent
// (the assistant), upload or undo.
Kind string `json:"kind"`
// Chat is the chat that wrote it, and ChatTitle its title when it still exists.
Chat string `json:"chat,omitempty"`
ChatTitle string `json:"chat_title,omitempty"`
Data map[string]any `json:"data,omitempty"`
Valid bool `json:"valid"`
Errors []string `json:"errors,omitempty"`
}
// ChatsWellKnownVersions lists the versions of a well-known file, newest first.
type ChatsWellKnownVersions struct {
Versions []ChatsWellKnownVersion `json:"versions"`
}
// ChatsWellKnownError is the answer to a write that was refused: the errors
// of a content that breaks the schema, or, on a conflict, what the file holds now.
type ChatsWellKnownError struct {
Error *APIError `json:"error"`
Errors []string `json:"errors,omitempty"`
Current *ChatsWellKnownFile `json:"current,omitempty"`
}
// ChatsEgressRule is one allow or deny rule of a chat's Sandbox network.
type ChatsEgressRule struct {
Host string `json:"host"`
Port int `json:"port"`
Allow bool `json:"allow"`
By string `json:"by,omitempty"`
Admin bool `json:"admin,omitempty"`
At time.Time `json:"at"`
// AboveLimit says the ceiling no longer admits the host, so the rule
// does not apply until an administrator raises it.
AboveLimit bool `json:"above_limit,omitempty"`
}
// ChatsEgressCard is a card that asked whether a command may reach a host.
type ChatsEgressCard struct {
ID string `json:"id"`
Host string `json:"host"`
Port int `json:"port"`
Kind string `json:"kind"`
State string `json:"state"` // pending, once, chat, denied, expired, refused
Private bool `json:"private,omitempty"`
// AboveCeiling says the administrator's limit does not admit this host,
// so no answer can make it work; only a higher limit does.
AboveCeiling bool `json:"above_ceiling,omitempty"`
ToolSeq int64 `json:"tool_seq,omitempty"`
AskedAt time.Time `json:"asked_at"`
AnsweredAt *time.Time `json:"answered_at,omitempty"`
AnsweredBy string `json:"answered_by,omitempty"`
}
// ChatsEgressRegistry is one package registry the preset registries admits.
type ChatsEgressRegistry struct {
ID string `json:"id"`
Title string `json:"title"`
Hosts []string `json:"hosts"`
}
// ChatsEgress is the network of a chat's Sandbox commands.
type ChatsEgress struct {
Preset string `json:"preset"` // off, registries, all
Ceiling string `json:"ceiling"` // the administrator's limit
Effective string `json:"effective"` // the lower of the two
// Applies is false when commands run on this machine (applies_reason
// host) or no Sandbox serves bash (none).
Applies bool `json:"applies"`
AppliesReason string `json:"applies_reason,omitempty"`
// Addon is the add-on that runs the chat's commands and carries the
// network-policy host control: the chat draws its Network row and links
// an administrator to that add-on's settings. Absent when applies is
// false or no such add-on runs the commands.
Addon string `json:"addon,omitempty" example:"sandbox"`
Registries []ChatsEgressRegistry `json:"registries"`
// AllowedHosts is the administrator's always-allowed list (host or
// host:port), read only here: it applies when the effective preset is
// registries or all.
AllowedHosts []string `json:"allowed_hosts"`
Rules []ChatsEgressRule `json:"rules"`
Cards []ChatsEgressCard `json:"cards"`
}
// ChatsEgressPreset sets the preset of a chat.
type ChatsEgressPreset struct {
Preset string `json:"preset"`
}
// ChatsEgressConsent answers a card: once (for the command in progress),
// chat (a rule for the chat) or deny (remembered for the chat).
type ChatsEgressConsent struct {
ID string `json:"id"`
Decision string `json:"decision"`
}
// ChatsEgressAdmin is the administrator's view of the Sandbox network.
type ChatsEgressAdmin struct {
Ceiling string `json:"ceiling"`
Default string `json:"default"`
AllowPrivate bool `json:"allow_private"`
Ports []int `json:"ports"`
ExtraRegistries []string `json:"extra_registries"`
AllowedHosts []string `json:"allowed_hosts"`
AuditDays int `json:"audit_days"`
Registries []ChatsEgressRegistry `json:"registries"`
// Sandbox is the add-on that runs commands, empty when none serves;
// Isolation is isolated, host, or empty when unknown.
Sandbox string `json:"sandbox,omitempty"`
Isolation string `json:"isolation,omitempty"`
Allowed int64 `json:"allowed_24h"`
Blocked int64 `json:"blocked_24h"`
}
// ChatsEgressEvent is one line of the Sandbox network audit.
type ChatsEgressEvent struct {
User string `json:"user"`
UserName string `json:"user_name,omitempty"`
Chat string `json:"chat"`
Host string `json:"host"`
Port int `json:"port"`
Kind string `json:"kind"`
Verdict string `json:"verdict"` // allow, block, done
Reason string `json:"reason"`
Addr string `json:"addr,omitempty"`
BytesUp int64 `json:"bytes_up,omitempty"`
BytesDown int64 `json:"bytes_down,omitempty"`
DurationMS int64 `json:"duration_ms,omitempty"`
At time.Time `json:"at"`
}
// ChatsEgressEventList is the audit, newest first.
type ChatsEgressEventList struct {
Events []ChatsEgressEvent `json:"events"`
}