mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-04 03:24:43 -05:00
* feat(addons): add a durable host policy store Keep bounded host policy in one owner-managed durable store. Use revision checks and retained receipts to reconcile updates without stale writes. Require a sole migration source and explicit owner selection for clusters. Restrict provider pins to tools and skills that existing consumers replace. Document the internal scope; UI and carrier integration remain separate. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): reject malformed host policy JSON Reject duplicate keys, noncanonical fields, and incomplete durable structures before JSON decoding can discard evidence of corruption. Validate provider keys before deletion so invalid removals cannot commit. Keep empty provider sets canonical when writing migrated policies. Regressions cover malformed documents, unchanged storage on rejection, and replay with case-sensitive operation IDs. Assisted-by: OpenAI:ChatGPT Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): decouple host policy validation Move shared egress parsers into a dependency-light package so the gateway can consume host settings without importing itself through core/config. Keep config wrappers and a host type alias to preserve existing callers. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(addons): carry host policy actions to owner Keep host policy reads and updates on the authenticated owner. The owner checks current administrator authority before updates and preserves CAS conflicts and durable operation receipts across forwarding. Provide read-only snapshots for trusted hosts over pinned mTLS. Missing owner services never fall back to frontend policy. Application startup, public settings routes and runtime consumers remain separate work. Verify typed actions with local and real mTLS forwarding tests, including revoked actors, strict payloads, receipt replay and unavailable stores. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): keep policy provenance private Return a bounded policy projection from gateway reads, updates and replay. Trusted host consumers use the same projection without migration metadata. The private owner store retains provenance for migration records. Cover local and pinned-carrier output, durable provenance and typed errors. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(addons): initialize owner host settings Open durable host settings only after the owner acquires its storage guard. Keep frontends forwarding without importing their local configuration. Preserve automatic migration for the sole combined host configuration. Require explicit owner-local selection for non-default dedicated policy. Close the store once on cancellation, shutdown or startup failure. Cover migration, restrictive defaults, restart revisions, corruption, duplicate owners and frontend isolation in the native bootstrap test. Document the owner-only migration option and storage constraints. Assisted-by: OpenAI:ChatGPT Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): close settings before the owner guard Startup failures released the owner guard before closing host settings. Use one deferred cleanup to preserve the close order on every failure. Test real runtime and gateway failures, shutdown, and cancellation. Check once-only closure and reacquire both locks after cleanup. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): enforce owner egress policy in Studio Read owner policy for Studio defaults, views, cards and enforcement. Frontend configuration must not override policy or mask an owner outage. Propagate context and policy errors instead of reusing a cached allow. Preserve local settings behavior when add-ons are disabled. Cover existing consumers through independent pinned mTLS carriers. Verify live tightening, pending-card outages and restart persistence. Document the read semantics and the proxy verdict-cache boundary. Assisted-by: OpenAI:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): read provider pins from owner policy Studio reads current owner pins for tools and skills instead of frontend configuration when add-ons are enabled. Policy failures stop selection rather than silently choosing built-ins. Disabled add-ons retain local configuration behavior. Cover owner updates and outages, and document the read contract. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(studio): abort turns on mandatory policy errors Sessions must not treat owner policy failures as optional tool outages. Stop priming before harness startup and abort turns before model sends. Preserve wrapped policy causes and existing optional connector behavior. Cover sent turns, priming failures, and optional connector failures. Document the failure contract without changing fallback or cache rules. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): use owner policy for auto-upgrades Read fresh owner policy before each automatic upgrade pass. Skip the pass when that policy is unavailable instead of trusting local settings. Wire a real owner policy store into the transportless manager fixture. Keep assertions for stale frontend settings and owner policy changes. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): guard legacy settings writes Reject migrated owner policy fields before applying any local settings. Omit those fields from legacy reads while add-ons are enabled, and keep unrelated local settings available without an owner request. Cover presence, case folding, duplicate keys and atomic rejection. Document the legacy contract and unchanged disabled behavior. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * test(addons): use the existing Ginkgo suites Register settings authority regressions with the existing suites to comply with the Ginkgo/Gomega test policy. Preserve the conflict, compatibility, owner outage, and upgrade assertions without adding another RunSpecs bootstrap. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat(egress): bound wildcard host policy grants Support descendant and public-any patterns through the shared parser. Keep wildcard grants inside the administrator ceiling and public guard. Preserve the legacy exact-host exception under the registries ceiling. Keep policy strings lossless for existing receipts and stored exact hosts. Test canonical matching, private exclusions, ports, and policy replay. Proxy connection authorization and cache changes remain separate. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(egress): authorize each new connection Remove cached allows so policy tightening and owner outages block new connections immediately. Keep existing DNS, private-address and SNI guards. Socket tests cover HTTP keep-alive, CONNECT and consumed grants. Established streams may finish under their original authorization. Expect the canonical runsc path in the gVisor test, matching production. This preserves the path contract when the test temp directory is a symlink. A short alias avoids Unix socket path limits in deep worktrees. Verify with go test -race -count=1 -timeout=180s ./addons/sandbox/... using an offline environment and a short alias to the worktree temp folder. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): save network policy through the owner Use owner revisions for host controls instead of frontend runtime settings. Keep drafts across conflicts and outages, and replay uncertain writes with the same operation identity. Read authoritative policy after each save. Accept bounded host wildcards while keeping extra registries exact. Keep typed private-access confirmation out of owner policy patches. Add rendered form and client tests, and document reconciliation behavior. Assisted-by: OpenAI: GPT-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): fail closed on owner policy errors Reject partial, mistyped, unknown and oversized owner snapshots before publishing them to the form. Match the complete Go policy JSON shape. Keep owner availability errors and uncertain operation identities when Discard resets the local draft. Only reconciliation can reopen saving. Tests reproduce both review failures and cover immutable retries, malformed reloads and the rendered discard-then-edit regression. Assisted-by: Codex:gpt-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): finish owner settings controls Route skill switches, provider choices and upgrade controls through owner policy. Preserve per-key changes when another frontend updates the policy. Keep local tool execution controls on their existing settings path. Extend the existing state checklist and add-on acceptance guidance so new controls declare their authority and test both deployment topologies. Assisted-by: nib:gpt-6.1-sol Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(studio): check policy errors in Tables skill tests Adapt the Tables test added on master to the error-aware skillTurn API. Assert that normal skill selection succeeds instead of discarding errors. Verified with make test-distributed-fakebus and scoped golangci-lint. Assisted-by: nib:gpt-6.1-sol Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(addons): document guarded settings IO boundaries Explain the narrow gosec exceptions for the authenticated administrator settings response and fixed owner-root files. Keep symlink, mode and type checks intact; no request-supplied paths reach this store. Explicitly ignore best-effort temporary-file and directory cleanup errors; file and directory sync failures still determine durability. Verified owner-store and handler race tests and scoped golangci-lint. Assisted-by: nib:gpt-6.1-sol Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * test(addons): run carrier policy cases in Ginkgo Register projection, forwarding, validation and CAS cases with the existing gateway suite. Check cleanup errors in test fixtures and explicitly ignore response-body cleanup after decoding in the internal read client. Verified gateway/application authority race tests and scoped lint. Assisted-by: nib:gpt-6.1-sol Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * test(addons): inventory owner policy gateway cases Require all four owner-policy cases in both the per-report and aggregate gateway and artifact acceptance inventories. Keep exact matching intact. Verified real test reports with the existing strict Ginkgo report validator: 215 gateway cases and 252 artifact cases passed, without skipped cases. The full browser readiness probe remains a separate CI gate. Assisted-by: nib:gpt-6.1-sol Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * test(ui): scope library settings apply action Select the exact Apply button in the runtime settings pending bar. The owner upgrade control has a separate Apply upgrade setting button, so an unscoped substring locator is ambiguous. Verified the failing Playwright case against the real UI test server and Google Chrome, with isolated server and browser temporary paths. Assisted-by: nib:gpt-6.1-sol Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * test(egress): update route and invalid-host fixtures Use the context-aware egress callback in route tests. Keep the last-valid list regression meaningful by using a partial-label wildcard, since a whole-label descendant pattern is now valid input. Verified focused application and route race tests and scoped lint. Assisted-by: nib:gpt-6.1-sol Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * test(settings): reject partial-label wildcard fixture Keep the invalid-input regression while accepting the newly supported whole-label descendant wildcard. Use api.*.example.com as invalid input. Reproduced the full endpoint suite failure, then verified all 616 endpoint specs pass and scoped lint is clean. Assisted-by: nib:gpt-6.1-sol Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
91 lines
5.9 KiB
Go
91 lines
5.9 KiB
Go
package studio
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/mudler/LocalAI/core/schema"
|
|
)
|
|
|
|
// HostExecConsent answers the card that asks to run one command on this
|
|
// machine.
|
|
// @Summary Allow or refuse one command of the built-in bash on this machine.
|
|
// @Description When chats_host_exec is ask, each command of the chat's built-in bash waits for an answer: a host_exec_consent event carries the id and the command. once runs it (administrators only); deny refuses it (anyone who owns the chat). The command runs with no isolation, as the user LocalAI runs as, with the network of the server. Without an answer the call ends after 5 minutes. A card waits only while the turn that asked runs; an answer to one whose turn has ended is 410 consent_expired.
|
|
// @Tags chats
|
|
// @Param id path string true "conversation id"
|
|
// @Param request body schema.ChatsHostExecConsent true "the id of the event and the answer"
|
|
// @Success 204 "The answer was stored with the card and sent to the running turn"
|
|
// @Failure 400 {object} schema.ErrorResponse "The request is not valid"
|
|
// @Failure 403 {object} schema.ErrorResponse "Only an administrator can allow a command"
|
|
// @Failure 404 {object} schema.ErrorResponse "No such conversation, or it is another user's"
|
|
// @Failure 409 {object} schema.ErrorResponse "The card was answered already with the other answer (already_answered); the same answer again is 204"
|
|
// @Failure 410 {object} schema.ErrorResponse "The card expired (consent_expired): the turn that asked has ended, its frontend stopped, or a newer card replaced it; the command did not run"
|
|
// @Router /api/chats/{id}/host-exec/consent [post]
|
|
func (h *Handlers) HostExecConsent(c echo.Context) error {
|
|
var in schema.ChatsHostExecConsent
|
|
if err := c.Bind(&in); err != nil {
|
|
return fail(c, http.StatusBadRequest, typeInvalid, "the body must be {id, decision}")
|
|
}
|
|
if err := h.svc.HostExecDecide(c.Request().Context(), userID(c), c.Param("id"), in.ID, in.Decision); err != nil {
|
|
return failFor(c, err)
|
|
}
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
// AdminHostExecEvents returns the audit of the commands the built-in bash ran.
|
|
// @Summary Read the audit of the commands run on this machine (administrators).
|
|
// @Description Every command the chats' built-in bash ran directly on this machine: who, in which chat, the command (cut at 4 KiB), the exit code (-1 when it did not exit by itself), how long it took and whether it timed out. The commands had the network of the server, so this is also the record of what they could reach.
|
|
// @Tags chats
|
|
// @Param limit query int false "How many lines, newest first (default 100, at most 500)"
|
|
// @Success 200 {object} schema.ChatsHostExecEventList "The newest commands first"
|
|
// @Router /api/chats/admin/host-exec/events [get]
|
|
func (h *Handlers) AdminHostExecEvents(c echo.Context) error {
|
|
ctx := c.Request().Context()
|
|
evs, err := h.svc.HostExecEvents(ctx, intParam(c, "limit", 100, 500))
|
|
if err != nil {
|
|
return failFor(c, err)
|
|
}
|
|
out := schema.ChatsHostExecEventList{Events: make([]schema.ChatsHostExecEvent, 0, len(evs))}
|
|
for _, e := range evs {
|
|
out.Events = append(out.Events, schema.ChatsHostExecEvent{User: e.User, UserName: h.svc.UserName(ctx, e.User), Chat: e.Chat, Command: e.Command,
|
|
Exit: e.Exit, DurationMS: e.DurationMS, TimedOut: e.TimedOut, At: e.At})
|
|
}
|
|
return c.JSON(http.StatusOK, out)
|
|
}
|
|
|
|
// AdminBuiltinTools lists the built-in chat tools for Settings > Tools.
|
|
// @Summary List the built-in chat tools (administrators).
|
|
// @Description The built-in tools of the chats (bash, web_search, web_fetch, as this server registers them): label, the per-chat switch group, the runtime setting that switches each off and the ones that configure it, whether it is on, whether it only reads and whether an add-on may replace it, who serves it now (builtin, an add-on's ID, or empty), the add-ons that replace it (oldest install first, with their install times in installed_at), the default one that serves with no pin (default: the oldest running install) and the one pinned with chats_tool_providers, the titles of the candidates, the candidates that are installed but stopped (stopped), a note "not running" when an installed add-on replaces the tool but none serves, and for bash the mode of chats_host_exec and why this server cannot run it, if so. skills lists the built-in skills (xlsx, docx, pptx, pdf) that an installed add-on replaces: who serves each now, the add-ons that replace it (the same order, default and installed_at) and the one pinned in the same chats_tool_providers map. Change them with POST /api/settings.
|
|
// @Tags chats
|
|
// @Success 200 {object} schema.ChatsBuiltinToolList "The built-in tools, by ID"
|
|
// @Router /api/chats/admin/tools [get]
|
|
func (h *Handlers) AdminBuiltinTools(c echo.Context) error {
|
|
list, err := h.svc.AdminBuiltinTools(c.Request().Context(), userID(c))
|
|
if err != nil {
|
|
return failFor(c, err)
|
|
}
|
|
out := schema.ChatsBuiltinToolList{Tools: make([]schema.ChatsBuiltinTool, 0, len(list))}
|
|
for _, t := range list {
|
|
cands := t.Candidates
|
|
if cands == nil {
|
|
cands = []string{}
|
|
}
|
|
opts := t.Options
|
|
if opts == nil {
|
|
opts = []string{}
|
|
}
|
|
out.Tools = append(out.Tools, schema.ChatsBuiltinTool{ID: t.ID, Label: t.Label, Description: t.Description, Group: t.Group, Setting: t.Setting,
|
|
Options: opts, ReadOnly: t.ReadOnly, Replaceable: t.Replaceable, Enabled: t.Enabled, Provider: t.Provider, Candidates: cands, Default: t.Default, Pinned: t.Pinned, InstalledAt: t.InstalledAt,
|
|
Titles: t.Titles, Stopped: t.Stopped, Note: t.Note, Mode: t.Mode, Unavailable: t.Unavailable})
|
|
}
|
|
out.Skills = []schema.ChatsBuiltinSkill{}
|
|
skills, err := h.svc.AdminBuiltinSkills(c.Request().Context(), userID(c))
|
|
if err != nil {
|
|
return failFor(c, err)
|
|
}
|
|
for _, sk := range skills {
|
|
out.Skills = append(out.Skills, schema.ChatsBuiltinSkill{ID: sk.ID, Provider: sk.Provider, Candidates: sk.Candidates, Default: sk.Default, Titles: sk.Titles, Pinned: sk.Pinned, InstalledAt: sk.InstalledAt, Note: sk.Note})
|
|
}
|
|
return c.JSON(http.StatusOK, out)
|
|
}
|