mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-05 20:13:23 -05:00
merge: bring master into the proposal UI branch
Union of both case lists in the add-on suite inventory; the swagger files are regenerated from the merged code. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
32 files changed
+73914
-451
No files matched your search
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"github.com/mudler/LocalAI/addons/internal/protocol"
|
||||
"github.com/mudler/LocalAI/addons/sandbox/internal/backends"
|
||||
"github.com/mudler/LocalAI/core/services/studio/sandbox"
|
||||
"github.com/mudler/LocalAI/core/services/studio/sandbox/runner"
|
||||
"github.com/mudler/LocalAI/core/services/studio/workspace/workfiles"
|
||||
"github.com/mudler/LocalAI/pkg/addons/contract"
|
||||
@@ -115,6 +116,18 @@ func (r *runtime) Close(ctx context.Context) error {
|
||||
|
||||
var errNoTool = errors.New("this tool is not served by the sandbox add-on")
|
||||
|
||||
// Execution is what the add-on declares about its bash tool: the host names the
|
||||
// provider by this title, tells the model the note when commands are isolated,
|
||||
// and offers the document skills of this package only while the status says so.
|
||||
func Execution() *contract.ExecutionDescriptor {
|
||||
return &contract.ExecutionDescriptor{
|
||||
Class: contract.ExecutionClassCommands,
|
||||
Title: "Sandbox",
|
||||
Note: sandbox.GuestToolsNote,
|
||||
Skills: &contract.ExecutionSkills{ProvidesRuntimeFor: []string{contract.SkillKindDocuments}},
|
||||
}
|
||||
}
|
||||
|
||||
// Definitions returns what a model sees of each tool. They come from the code
|
||||
// tools themselves, with the strictness the contract asks of an arguments
|
||||
// schema.
|
||||
@@ -133,6 +146,7 @@ func Definitions() []contract.ToolDefinition {
|
||||
def := contract.ToolDefinition{Name: t.Name, Kind: contract.ToolKindAction, Description: t.Description, InputSchema: raw}
|
||||
if t.Name == ShadowedTool {
|
||||
def.Shadows = ShadowedTool
|
||||
def.Execution = Execution()
|
||||
}
|
||||
out = append(out, def)
|
||||
}
|
||||
|
||||
@@ -261,6 +261,7 @@ var _ = Describe("Sandbox tools", func() {
|
||||
// add-on's own page, with the default grant of egress.check.
|
||||
Expect(m.Capabilities.UI.Controls).To(Equal([]string{contract.ControlNetworkPolicy, contract.ControlToolProviders}))
|
||||
Expect(contract.EntitledControls(m, []string{"workspace.", "egress."})).To(Equal(m.Capabilities.UI.Controls))
|
||||
Expect(m.API.Max).To(BeNumerically(">=", contract.MinExecutionAPI), "the execution descriptor needs api 2")
|
||||
want := tools.Definitions()
|
||||
Expect(m.Capabilities.ToolDefinitions).To(HaveLen(len(want)))
|
||||
for i, d := range m.Capabilities.ToolDefinitions {
|
||||
@@ -272,6 +273,7 @@ var _ = Describe("Sandbox tools", func() {
|
||||
Expect(d.Description).To(Equal(want[i].Description), "definition %s differs from the runtime", d.Name)
|
||||
Expect(d.UIOnly).To(Equal(want[i].UIOnly), "definition %s differs from the runtime", d.Name)
|
||||
Expect(d.Shadows).To(Equal(want[i].Shadows), "definition %s differs from the runtime", d.Name)
|
||||
Expect(d.Execution).To(Equal(want[i].Execution), "definition %s differs from the runtime", d.Name)
|
||||
Expect(a).To(Equal(b), "definition %s differs from the runtime", d.Name)
|
||||
Expect(contract.ValidateToolSchema(d.InputSchema, d.Kind)).To(Succeed(), "schema of %s", d.Name)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"api": {
|
||||
"max": 1,
|
||||
"max": 2,
|
||||
"min": 1
|
||||
},
|
||||
"arch": "amd64",
|
||||
@@ -41,7 +41,17 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"shadows": "bash"
|
||||
"shadows": "bash",
|
||||
"execution": {
|
||||
"class": "commands",
|
||||
"title": "Sandbox",
|
||||
"note": "The sandbox has: python3 (pandas, openpyxl, docx, pptx, pypdf, Pillow), pip, jq, ripgrep (rg), sqlite3, zip, unzip, file, tree, poppler (pdftotext, pdftoppm), git, curl. It has no Node, ImageMagick, ffmpeg or OCR, and apt cannot install anything.",
|
||||
"skills": {
|
||||
"providesRuntimeFor": [
|
||||
"documents"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "execution-status",
|
||||
|
||||
@@ -751,7 +751,7 @@ func (s *studioAddons) Tools(ctx context.Context, user string) ([]studio.AddonTo
|
||||
continue
|
||||
}
|
||||
out = append(out, studio.AddonTool{Name: d.Name, AddonID: d.AddonID, Description: d.Description, Schema: raw, Action: d.Kind != contract.ToolKindRead,
|
||||
Shadows: d.Shadows, InstalledAt: d.InstalledAt})
|
||||
Shadows: d.Shadows, Execution: d.Execution, InstalledAt: d.InstalledAt})
|
||||
}
|
||||
return filterByFeature(out, s.hostOps, s.has, actor), nil
|
||||
}
|
||||
@@ -777,7 +777,7 @@ func (s *studioAddons) AddonSkills(ctx context.Context, user string) ([]studio.A
|
||||
if denied[a.AddonID] {
|
||||
continue
|
||||
}
|
||||
out = append(out, studio.AddonSkillSet{AddonID: a.AddonID, Title: a.Title, InstalledAt: a.InstalledAt, Skills: contract.SkillNames(a.Skills),
|
||||
out = append(out, studio.AddonSkillSet{AddonID: a.AddonID, Title: a.Title, InstalledAt: a.InstalledAt, Skills: contract.SkillNames(a.Skills), Isolated: a.Isolated,
|
||||
Shadows: contract.ShadowedSkills(a.Skills), Running: a.Running})
|
||||
}
|
||||
return out, nil
|
||||
|
||||
@@ -979,7 +979,7 @@ func (a *Application) startAddons() error {
|
||||
}
|
||||
host := c.Host
|
||||
if host == (contract.HostCompatibility{}) {
|
||||
host = contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: contract.HostSDKVersion, DataFormat: 1, OS: runtime.GOOS, Arch: runtime.GOARCH}
|
||||
host = contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, APIMin: contract.HostAPIMin, Protocol: 1, SDK: contract.HostSDKVersion, DataFormat: 1, OS: runtime.GOOS, Arch: runtime.GOARCH}
|
||||
}
|
||||
cat = catalog.Catalog{Sources: sources, TargetOS: host.OS, TargetArch: host.Arch}
|
||||
installer = &packages.Installer{Root: filepath.Join(c.Root, "packages"), Catalog: cat, Host: host}
|
||||
|
||||
@@ -388,8 +388,8 @@ var _ = Describe("apps routes outcomes and documentation", Label("apps"), func()
|
||||
Expect(json.Unmarshal(b, &m)).To(Succeed())
|
||||
return m
|
||||
}
|
||||
full := apps.App{ID: "a", Name: "a", Title: "t", Titles: map[string]string{"en": "t"}, Description: "d", Descriptions: map[string]string{"en": "d"}, Icon: "i", Version: "1", InstalledAt: "2026-10-30T09:15:00Z", Generation: 1, Enabled: true, State: "running", Update: "update-pending", MainPage: "/m", HasUI: true, LaunchMode: "always", Tools: []string{"x"}, ToolCount: 1, Shadows: []string{"bash"}, ToolDefinitions: []contract.ToolDefinition{{Name: "x", Description: "d", Kind: "read", InputSchema: json.RawMessage(`{"type":"object"}`), UIOnly: true, Shadows: "bash"}}, Skills: []contract.SkillDefinition{{Name: "xlsx", Shadows: "xlsx"}}, SkillShadows: []string{"xlsx"}, UI: &apps.UIView{Entry: "/e", CSS: []string{"/c"}, SDK: contract.VersionRange{Min: 1, Max: 1}, Actions: []apps.ChatActionView{{ID: "a", Label: "A", Labels: map[string]string{"en": "A"}, Icon: "star", PackagedIcon: true, Placement: "composer", Tool: "t", Component: "C"}}, Slots: []apps.SlotView{{ID: "s", Slot: "files.preview", Component: "C", Extensions: []string{"docx"}}}, Renderers: []contract.Renderer{{Kind: "approval-card", Tool: "t", Component: "C"}}, Pages: []string{"p"}, Navigation: []string{"p"}, ChatActions: []string{"a"}, Controls: []string{"network-policy"}}, Admin: &apps.AdminView{CanStart: true, CanStop: true, ManagementURL: "/x"}}
|
||||
for name, v := range map[string]any{"apps.App": full, "apps.AdminView": *full.Admin, "apps.UIView": *full.UI, "apps.ChatActionView": full.UI.Actions[0], "apps.SlotView": full.UI.Slots[0], "contract.Renderer": full.UI.Renderers[0], "contract.VersionRange": full.UI.SDK, "contract.ToolDefinition": full.ToolDefinitions[0], "contract.SkillDefinition": full.Skills[0], "apps.List": apps.List{Apps: []apps.App{full}}} {
|
||||
full := apps.App{ID: "a", Name: "a", Title: "t", Titles: map[string]string{"en": "t"}, Description: "d", Descriptions: map[string]string{"en": "d"}, Icon: "i", Version: "1", InstalledAt: "2026-10-30T09:15:00Z", Generation: 1, Enabled: true, State: "running", Update: "update-pending", MainPage: "/m", HasUI: true, LaunchMode: "always", Tools: []string{"x"}, ToolCount: 1, Shadows: []string{"bash"}, ToolDefinitions: []contract.ToolDefinition{{Name: "x", Description: "d", Kind: "read", InputSchema: json.RawMessage(`{"type":"object"}`), UIOnly: true, Shadows: "bash", Execution: &contract.ExecutionDescriptor{Class: "commands", Title: "T", Note: "n", Skills: &contract.ExecutionSkills{ProvidesRuntimeFor: []string{"documents"}}}}}, Skills: []contract.SkillDefinition{{Name: "xlsx", Shadows: "xlsx"}}, SkillShadows: []string{"xlsx"}, UI: &apps.UIView{Entry: "/e", CSS: []string{"/c"}, SDK: contract.VersionRange{Min: 1, Max: 1}, Actions: []apps.ChatActionView{{ID: "a", Label: "A", Labels: map[string]string{"en": "A"}, Icon: "star", PackagedIcon: true, Placement: "composer", Tool: "t", Component: "C"}}, Slots: []apps.SlotView{{ID: "s", Slot: "files.preview", Component: "C", Extensions: []string{"docx"}}}, Renderers: []contract.Renderer{{Kind: "approval-card", Tool: "t", Component: "C"}}, Pages: []string{"p"}, Navigation: []string{"p"}, ChatActions: []string{"a"}, Controls: []string{"network-policy"}}, Admin: &apps.AdminView{CanStart: true, CanStop: true, ManagementURL: "/x"}}
|
||||
for name, v := range map[string]any{"apps.App": full, "apps.AdminView": *full.Admin, "apps.UIView": *full.UI, "apps.ChatActionView": full.UI.Actions[0], "apps.SlotView": full.UI.Slots[0], "contract.Renderer": full.UI.Renderers[0], "contract.VersionRange": full.UI.SDK, "contract.ToolDefinition": full.ToolDefinitions[0], "contract.ExecutionDescriptor": *full.ToolDefinitions[0].Execution, "contract.ExecutionSkills": *full.ToolDefinitions[0].Execution.Skills, "contract.SkillDefinition": full.Skills[0], "apps.List": apps.List{Apps: []apps.App{full}}} {
|
||||
def, ok := doc.Definitions[name]
|
||||
Expect(ok).To(BeTrue(), name)
|
||||
shape := emitted(v)
|
||||
|
||||
@@ -284,7 +284,7 @@ func (s *Supervisor) start(ctx context.Context, pkg packages.PackageReceipt, gen
|
||||
func (s *Supervisor) launch(ctx context.Context, pkg packages.PackageReceipt, generation uint64) (*process, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, s.config.ReadyTimeout)
|
||||
defer cancel()
|
||||
host := contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: contract.HostSDKVersion, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH}
|
||||
host := contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, APIMin: contract.HostAPIMin, Protocol: 1, SDK: contract.HostSDKVersion, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH}
|
||||
if contract.ValidateManifest(pkg.Manifest, host) != nil || !filepath.IsAbs(pkg.Directory) {
|
||||
return nil, ErrRuntime
|
||||
}
|
||||
|
||||
@@ -113,6 +113,10 @@ type ToolDefinition struct {
|
||||
// contract.ToolDefinition.Shadows), or empty. Only the chat consumer acts
|
||||
// on it; every other consumer keeps the namespaced Name.
|
||||
Shadows string `json:"shadows,omitempty"`
|
||||
// Execution is what the add-on declares about a tool that shadows bash. It
|
||||
// is never nil on such a tool: a manifest without a descriptor gets one
|
||||
// with the add-on's title.
|
||||
Execution *contract.ExecutionDescriptor `json:"execution,omitempty"`
|
||||
// InstalledAt is when the add-on was first installed (RFC 3339, UTC), or
|
||||
// empty. The chat consumer ranks the add-ons that shadow one name by it.
|
||||
InstalledAt string `json:"installedAt,omitempty"`
|
||||
@@ -400,6 +404,9 @@ type AddonSkills struct {
|
||||
// InstalledAt is when the add-on was first installed, or empty.
|
||||
InstalledAt string
|
||||
Skills []contract.SkillDefinition
|
||||
// Isolated are the skills that need the add-on's own commands tool to
|
||||
// report isolated execution (see contract.IsolatedSkills).
|
||||
Isolated []string
|
||||
// Running is true when the add-on may serve now: enabled and running, as
|
||||
// Discover requires for tools.
|
||||
Running bool
|
||||
@@ -423,7 +430,8 @@ func (t *ToolAdapter) Skills(ctx context.Context, actor contract.Actor) ([]Addon
|
||||
continue
|
||||
}
|
||||
running := a.Enabled && a.State == "running" && a.Generation > 0
|
||||
out = append(out, AddonSkills{AddonID: a.ID, Title: a.Title, InstalledAt: a.InstalledAt, Skills: slices.Clone(a.Skills), Running: running})
|
||||
out = append(out, AddonSkills{AddonID: a.ID, Title: a.Title, InstalledAt: a.InstalledAt, Skills: slices.Clone(a.Skills),
|
||||
Isolated: contract.IsolatedSkills(a.ToolDefinitions, a.Skills), Running: running})
|
||||
}
|
||||
slices.SortFunc(out, func(x, y AddonSkills) int { return strings.Compare(x.AddonID, y.AddonID) })
|
||||
return out, nil
|
||||
@@ -496,10 +504,27 @@ func definition(a apps.App, d contract.ToolDefinition) (ToolDefinition, bool) {
|
||||
// shadows it.
|
||||
if d.Shadows != "" && contract.ValidateShadow(d) == nil {
|
||||
def.Shadows = d.Shadows
|
||||
if d.Shadows == "bash" {
|
||||
def.Execution = executionOf(a, d)
|
||||
}
|
||||
}
|
||||
return def, true
|
||||
}
|
||||
|
||||
// executionOf is the descriptor of a commands tool as the chat consumer sees
|
||||
// it. A manifest that declares none behaves as a provider of commands under the
|
||||
// add-on's title.
|
||||
func executionOf(a apps.App, d contract.ToolDefinition) *contract.ExecutionDescriptor {
|
||||
e := contract.ExecutionDescriptor{Class: contract.ExecutionClassCommands}
|
||||
if d.Execution != nil {
|
||||
e = *d.Execution
|
||||
}
|
||||
if e.Title == "" {
|
||||
e.Title = a.Title
|
||||
}
|
||||
return &e
|
||||
}
|
||||
|
||||
// Discover returns the tools the actor may offer a model now, in a stable
|
||||
// order. A model is never told about a tool it could not call.
|
||||
func (t *ToolAdapter) Discover(ctx context.Context, actor contract.Actor) ([]ToolDefinition, error) {
|
||||
|
||||
@@ -187,6 +187,38 @@ var _ = Describe("A tool that shadows a built-in chat tool", func() {
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("The execution descriptor of a commands tool", func() {
|
||||
ctx := context.Background()
|
||||
schema := json.RawMessage(`{"type":"object","properties":{"command":{"type":"string"}},"required":["command"],"additionalProperties":false}`)
|
||||
bashTool := func(e *contract.ExecutionDescriptor) contract.ToolDefinition {
|
||||
return contract.ToolDefinition{Name: "bash", Kind: contract.ToolKindAction, Description: "Run.", InputSchema: schema, Shadows: "bash", Execution: e}
|
||||
}
|
||||
discover := func(app apps.App) tools.ToolDefinition {
|
||||
defs, err := tools.NewChatToolAdapter(&fake{list: apps.List{Apps: []apps.App{app}}}).Discover(ctx, member)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(defs).To(HaveLen(1))
|
||||
return defs[0]
|
||||
}
|
||||
It("reaches the chat as the add-on declared it", func() {
|
||||
e := &contract.ExecutionDescriptor{Class: contract.ExecutionClassCommands, Title: "Runbox", Note: "It has jq."}
|
||||
d := discover(apps.App{ID: "runbox", Title: "Run box", Generation: 1, Enabled: true, State: "running", Tools: []string{"bash"}, ToolDefinitions: []contract.ToolDefinition{bashTool(e)}})
|
||||
Expect(d.Execution).To(Equal(e))
|
||||
})
|
||||
It("falls back to the add-on's title for a manifest that declares none", func() {
|
||||
d := discover(apps.App{ID: "runbox", Title: "Run box", Generation: 1, Enabled: true, State: "running", Tools: []string{"bash"}, ToolDefinitions: []contract.ToolDefinition{bashTool(nil)}})
|
||||
Expect(d.Execution).To(Equal(&contract.ExecutionDescriptor{Class: contract.ExecutionClassCommands, Title: "Run box"}))
|
||||
})
|
||||
It("lists the skills that need isolation for the add-on that owns the commands tool", func() {
|
||||
skills := []contract.SkillDefinition{{Name: "docx", Shadows: "docx"}, {Name: "runbox-tips"}}
|
||||
app := apps.App{ID: "runbox", Title: "Run box", Generation: 1, Enabled: true, State: "running", Tools: []string{"bash"}, Skills: skills,
|
||||
ToolDefinitions: []contract.ToolDefinition{bashTool(nil)}}
|
||||
list, err := tools.NewChatToolAdapter(&fake{list: apps.List{Apps: []apps.App{app}}}).Skills(ctx, member)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(list).To(HaveLen(1))
|
||||
Expect(list[0].Isolated).To(Equal([]string{"docx"}))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("The add-ons that replace a built-in chat tool", func() {
|
||||
ctx := context.Background()
|
||||
bash := json.RawMessage(`{"type":"object","properties":{"command":{"type":"string"}},"required":["command"],"additionalProperties":false}`)
|
||||
|
||||
@@ -38,6 +38,9 @@ type AddonSkillSet struct {
|
||||
// InstalledAt is when the add-on was first installed, or empty.
|
||||
InstalledAt string
|
||||
Skills []string
|
||||
// Isolated are the skills that run only when the add-on's own commands tool
|
||||
// reports isolated execution (a subset of Skills).
|
||||
Isolated []string
|
||||
// Shadows are the built-in skills it replaces (a subset of Skills).
|
||||
Shadows []string
|
||||
// Running is true when the add-on can serve now: installed, enabled and
|
||||
@@ -61,14 +64,15 @@ type addonSkills struct {
|
||||
known []AddonSkillSet
|
||||
// pins are the administrator's choices, by name.
|
||||
pins map[string]string
|
||||
// hiddenDocuments are the document skills of the Sandbox left out because
|
||||
// it runs the chat's commands without isolation.
|
||||
// hiddenDocuments are the document skills left out because the add-on
|
||||
// that ships them runs the chat's commands without isolation.
|
||||
hiddenDocuments []string
|
||||
// documentsWhy is why the Sandbox is not known to be isolated.
|
||||
documentsWhy string
|
||||
// documentsWhy is why that add-on is not known to be isolated, and
|
||||
// documentsBy its title.
|
||||
documentsWhy, documentsBy string
|
||||
}
|
||||
|
||||
// withNotes adds to notes the line that says why the Sandbox document skills
|
||||
// withNotes adds to notes the line that says why the document skills
|
||||
// are missing, so the person is not left to guess.
|
||||
// documentNotePrefix starts the note that explains the missing document skills.
|
||||
const documentNotePrefix = "the document skills "
|
||||
@@ -77,7 +81,7 @@ func (a addonSkills) withNotes(notes []string) []string {
|
||||
if len(a.hiddenDocuments) == 0 {
|
||||
return notes
|
||||
}
|
||||
return append(notes, documentNotePrefix+strings.Join(a.hiddenDocuments, ", ")+" need an isolated Sandbox; "+a.documentsWhy)
|
||||
return append(notes, documentNotePrefix+strings.Join(a.hiddenDocuments, ", ")+" need isolated command execution by "+a.documentsBy+"; "+a.documentsWhy)
|
||||
}
|
||||
|
||||
// addonSkillsFor lists the add-ons of the user that ship skills and decides
|
||||
@@ -118,13 +122,15 @@ func (s *Service) addonSkillsFor(ctx context.Context, p string, addonsOff []stri
|
||||
if gate != nil && gate.Unavailable(ctx, p, set.AddonID, s.d.Files.Available()) != "" {
|
||||
continue
|
||||
}
|
||||
if set.AddonID == "sandbox" && !sandboxDocumentEligible(command) {
|
||||
if command.State == CommandsAvailable && command.Provider == "sandbox" {
|
||||
if len(set.Isolated) > 0 && !isolatedProvider(command, set.AddonID) {
|
||||
needs := func(n string) bool { return slices.Contains(set.Isolated, n) }
|
||||
if command.State == CommandsAvailable && command.Provider == set.AddonID {
|
||||
out.documentsWhy = s.documentsWhy(ctx, p, command)
|
||||
out.hiddenDocuments = append(out.hiddenDocuments, slices.DeleteFunc(slices.Clone(set.Skills), func(n string) bool { return !contract.DocumentSkill(n) })...)
|
||||
out.documentsBy = command.Title
|
||||
out.hiddenDocuments = append(out.hiddenDocuments, slices.DeleteFunc(slices.Clone(set.Skills), func(n string) bool { return !needs(n) })...)
|
||||
}
|
||||
set.Skills = slices.DeleteFunc(slices.Clone(set.Skills), contract.DocumentSkill)
|
||||
set.Shadows = slices.DeleteFunc(slices.Clone(set.Shadows), contract.DocumentSkill)
|
||||
set.Skills = slices.DeleteFunc(slices.Clone(set.Skills), needs)
|
||||
set.Shadows = slices.DeleteFunc(slices.Clone(set.Shadows), needs)
|
||||
}
|
||||
out.usable = append(out.usable, set)
|
||||
}
|
||||
@@ -239,7 +245,7 @@ func (a addonSkills) markFallbacks(list []skills.Skill) string {
|
||||
return "The built-in skills " + strings.Join(parts, ", ") + " serve because the add-on's own skill is off or not running in this chat."
|
||||
}
|
||||
|
||||
// documentsWhy says why the Sandbox that runs the chat's commands is not known
|
||||
// documentsWhy says why the add-on that runs the chat's commands is not known
|
||||
// to be isolated: it runs them on the host, or its status could not be read.
|
||||
func (s *Service) documentsWhy(ctx context.Context, p string, command Commands) string {
|
||||
if command.Isolation == "host" {
|
||||
@@ -247,7 +253,7 @@ func (s *Service) documentsWhy(ctx context.Context, p string, command Commands)
|
||||
}
|
||||
value, d := ObserveExecution(ctx, p, command.Addon)
|
||||
if value.Readiness == "unavailable" {
|
||||
return "the Sandbox reports that it cannot run commands"
|
||||
return commandsTitle(command) + " reports that it cannot run commands"
|
||||
}
|
||||
why := d.Failure.Text()
|
||||
if d.Failure == "" {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
addontools "github.com/mudler/LocalAI/core/services/addons/tools"
|
||||
"github.com/mudler/LocalAI/core/services/studio/session"
|
||||
"github.com/mudler/LocalAI/core/services/studio/workspace"
|
||||
"github.com/mudler/LocalAI/pkg/addons/contract"
|
||||
)
|
||||
|
||||
// AddonTool is one tool of an installed add-on, as a chat offers it to the model.
|
||||
@@ -27,6 +28,9 @@ type AddonTool struct {
|
||||
// empty: the tool keeps its add-on name. A tool that shadows bash is the
|
||||
// add-on's tool that runs shell commands and code.
|
||||
Shadows string
|
||||
// Execution is what the add-on declares about a tool that shadows bash
|
||||
// (its provider title and note), or nil.
|
||||
Execution *contract.ExecutionDescriptor
|
||||
// InstalledAt is when the add-on was first installed (RFC 3339, UTC), or
|
||||
// empty. It decides the default provider of a shadowed name.
|
||||
InstalledAt string
|
||||
|
||||
@@ -9,10 +9,10 @@ import (
|
||||
"time"
|
||||
|
||||
addontools "github.com/mudler/LocalAI/core/services/addons/tools"
|
||||
"github.com/mudler/LocalAI/core/services/studio/sandbox"
|
||||
"github.com/mudler/LocalAI/core/services/studio/session"
|
||||
"github.com/mudler/LocalAI/core/services/studio/skills"
|
||||
"github.com/mudler/LocalAI/core/services/studio/workspace"
|
||||
"github.com/mudler/LocalAI/pkg/addons/contract"
|
||||
)
|
||||
|
||||
// A chat that has no tool to run commands must say so. Without that fact a
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
// CommandIsolation is implemented by a broker that can tell how the tool that
|
||||
// runs commands is isolated. It is optional.
|
||||
type CommandIsolation interface {
|
||||
// Isolation is "isolated" when commands run in a sandbox, "host" when they
|
||||
// Isolation is "isolated" when commands run in an isolated environment, "host" when they
|
||||
// run directly on the server with no isolation, and "" when the broker
|
||||
// cannot tell.
|
||||
Isolation(ctx context.Context, user, addonID string) string
|
||||
@@ -73,6 +73,12 @@ type Commands struct {
|
||||
// Addon is the add-on that runs commands, or that would (off,
|
||||
// unavailable).
|
||||
Addon string
|
||||
// Title names the add-on in the text the model and the person read: the
|
||||
// title its commands tool declares, or the add-on's own.
|
||||
Title string
|
||||
// Note is the sentence the add-on declares for the model when its commands
|
||||
// are isolated, or "".
|
||||
Note string
|
||||
// Provider serves the tool when State is available: the add-on's ID, or
|
||||
// BuiltinProvider for the built-in bash on this machine.
|
||||
Provider string
|
||||
@@ -132,6 +138,17 @@ func addonRunner(tools []session.Tool) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// commandsDeclared is what the add-on that serves the commands in tools
|
||||
// declares about its commands tool.
|
||||
func commandsDeclared(tools []session.Tool, addon string) *contract.ExecutionDescriptor {
|
||||
for _, t := range tools {
|
||||
if t.Provider == addon && t.Execution != nil {
|
||||
return t.Execution
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// standInRunner returns the add-on whose stand-in tool (the add-on cannot
|
||||
// work) is in the list, when that add-on replaces bash.
|
||||
func standInRunner(tools []session.Tool) string {
|
||||
@@ -174,19 +191,47 @@ func (s *Service) commandAddons(ctx context.Context, user string) []string {
|
||||
return OrderProviders(ids, installed)
|
||||
}
|
||||
|
||||
// commandTitle is the name of an add-on that is not serving commands now: the
|
||||
// title of its manifest when the broker lists add-ons in any state, else the
|
||||
// title its commands tool declares, else its ID.
|
||||
func (s *Service) commandTitle(ctx context.Context, user, id string) string {
|
||||
if _, ok := s.d.AddonTools.(ShadowLister); ok {
|
||||
if t := s.shadowersFor(ctx, user).title[id]; t != "" {
|
||||
return t
|
||||
}
|
||||
}
|
||||
if s.d.AddonTools == nil {
|
||||
return id
|
||||
}
|
||||
if defs, err := s.d.AddonTools.Tools(ctx, user); err == nil {
|
||||
for _, d := range defs {
|
||||
if d.AddonID == id && d.Execution != nil && d.Execution.Title != "" {
|
||||
return d.Execution.Title
|
||||
}
|
||||
}
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// commandsOf says whether the tools of a chat can run commands. addonsOff are
|
||||
// the add-ons the chat switched off and sc the agent's scope, for a chat that
|
||||
// cannot: they tell an add-on that is installed but off from none at all.
|
||||
func (s *Service) commandsOf(ctx context.Context, user string, tools []session.Tool, addonsOff []string, sc *scope) Commands {
|
||||
if addon := addonRunner(tools); addon != "" {
|
||||
c := Commands{State: CommandsAvailable, Tool: commandTool, Addon: addon}
|
||||
c := Commands{State: CommandsAvailable, Tool: commandTool, Addon: addon, Title: addon}
|
||||
if e := commandsDeclared(tools, addon); e != nil {
|
||||
c.Note = e.Note
|
||||
if e.Title != "" {
|
||||
c.Title = e.Title
|
||||
}
|
||||
}
|
||||
if iso, ok := s.d.AddonTools.(CommandIsolation); ok {
|
||||
c.Isolation = iso.Isolation(ctx, user, addon)
|
||||
}
|
||||
return c
|
||||
}
|
||||
if addon := standInRunner(tools); addon != "" {
|
||||
c := Commands{State: CommandsUnavailable, Addon: addon}
|
||||
c := Commands{State: CommandsUnavailable, Addon: addon, Title: s.commandTitle(ctx, user, addon)}
|
||||
if gate, ok := s.d.AddonTools.(AddonGate); ok {
|
||||
c.Reason = gate.Unavailable(ctx, user, addon, s.d.Files.Available())
|
||||
}
|
||||
@@ -195,7 +240,7 @@ func (s *Service) commandsOf(ctx context.Context, user string, tools []session.T
|
||||
off := set(addonsOff)
|
||||
for _, id := range s.commandAddons(ctx, user) {
|
||||
if off[id] || (sc != nil && !sc.addons[id]) {
|
||||
return Commands{State: CommandsOff, Addon: id}
|
||||
return Commands{State: CommandsOff, Addon: id, Title: s.commandTitle(ctx, user, id)}
|
||||
}
|
||||
}
|
||||
return Commands{State: CommandsNone}
|
||||
@@ -242,13 +287,13 @@ func (s *Service) commandsFor(ctx context.Context, p string, rec session.ChatRec
|
||||
return shellSwitched(s.withHostBash(c, tools), s.hiddenFor(ctx, rec)), nil
|
||||
}
|
||||
|
||||
// sandboxWhere is where a command of the Sandbox starts and what it keeps.
|
||||
// commandsWhere is where a command of an add-on starts and what it keeps.
|
||||
// fileTools says whether the chat has the file tools, which see the same
|
||||
// files: a chat without them must not hear of them. A scoped chat (its agent
|
||||
// works in one folder) has that folder at /workspace/home, at the agent's
|
||||
// mode, and its chat folder at /workspace/chat; its shared folders are read
|
||||
// only.
|
||||
func sandboxWhere(fileTools bool, place session.Place) string {
|
||||
func commandsWhere(fileTools bool, place session.Place) string {
|
||||
same := ""
|
||||
if fileTools {
|
||||
same = ", the same files as the file tools"
|
||||
@@ -263,10 +308,18 @@ func sandboxWhere(fileTools bool, place session.Place) string {
|
||||
return "It starts in /workspace/home, the user's home" + same + "; only what it saves under /workspace/home or a writable shared folder is kept."
|
||||
}
|
||||
|
||||
// skillsNotInSandbox is the sentence that keeps a model from looking for a
|
||||
// skill folder in the Sandbox, which sees the user's home only, and points it
|
||||
// at the tool that loads a skill by its name.
|
||||
const skillsNotInSandbox = "Skill folders are not visible in the Sandbox: load skills with load_skill."
|
||||
// skillsNotInProvider is the sentence that keeps a model from looking for a
|
||||
// skill folder in an isolated environment, which sees the user's home only, and
|
||||
// points it at the tool that loads a skill by its name.
|
||||
const skillsNotInProvider = "Skill folders are not visible in the isolated environment: load skills with load_skill."
|
||||
|
||||
// commandsTitle is the name the prompt gives the add-on that runs commands.
|
||||
func commandsTitle(c Commands) string {
|
||||
if c.Title != "" {
|
||||
return c.Title
|
||||
}
|
||||
return "the add-on"
|
||||
}
|
||||
|
||||
// commandsBlock is the block of the system prompt that states whether the chat
|
||||
// can run commands. writeFile says whether the chat has write_file,
|
||||
@@ -292,11 +345,15 @@ func commandsBlock(c Commands, writeFile, fileTools, skillsOff bool, place sessi
|
||||
case c.Isolation == "isolated":
|
||||
// The turn's note is cut at 400 characters and the tool description
|
||||
// is capped, so the list of programs goes here, in the prompt.
|
||||
return "Commands: " + c.Tool + " runs commands and code in an isolated sandbox. " + sandboxWhere(fileTools, place) + " " + sandbox.GuestToolsNote + " " + skillsNotInSandbox
|
||||
b := "Commands: " + c.Tool + " runs commands and code in an isolated environment (" + commandsTitle(c) + "). " + commandsWhere(fileTools, place) + " "
|
||||
if c.Note != "" {
|
||||
b += c.Note + " "
|
||||
}
|
||||
return b + skillsNotInProvider
|
||||
case c.Isolation == "host":
|
||||
return "Commands: " + c.Tool + " runs commands directly on the server, with no isolation. Ask before you delete or change anything outside the task. " + sandboxWhere(fileTools, place)
|
||||
return "Commands: " + c.Tool + " runs commands directly on the server, with no isolation. Ask before you delete or change anything outside the task. " + commandsWhere(fileTools, place)
|
||||
}
|
||||
return "Commands: " + c.Tool + " runs commands and code. " + sandboxWhere(fileTools, place)
|
||||
return "Commands: " + c.Tool + " runs commands and code. " + commandsWhere(fileTools, place)
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("Commands: nothing in this chat runs code or commands. Never claim or imply that you ran anything, and do not ask a sub-agent to run it: it has the same tools. Instead ")
|
||||
@@ -308,11 +365,11 @@ func commandsBlock(c Commands, writeFile, fileTools, skillsOff bool, place sessi
|
||||
case CommandsSwitchedOff:
|
||||
b.WriteString("they switched running commands off for this chat and can switch Run commands on in its Tools menu.")
|
||||
case CommandsOff:
|
||||
b.WriteString("they can switch the Sandbox add-on on in the Tools menu of this chat.")
|
||||
b.WriteString("they can switch the " + commandsTitle(c) + " add-on on in the Tools menu of this chat.")
|
||||
case CommandsUnavailable:
|
||||
b.WriteString("the Sandbox add-on cannot run commands on this server; an administrator can fix that.")
|
||||
b.WriteString("the " + commandsTitle(c) + " add-on cannot run commands on this server; an administrator can fix that.")
|
||||
default:
|
||||
b.WriteString("an administrator can enable running code by installing the Sandbox add-on.")
|
||||
b.WriteString("an administrator can enable running code by installing an add-on that runs commands.")
|
||||
}
|
||||
if skillsOff {
|
||||
b.WriteString(" A skill marked unavailable here needs this: say so instead of starting it.")
|
||||
@@ -426,10 +483,13 @@ func (s *Service) repeatedTask(ctx context.Context, a workspace.Actor, task stri
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// sandboxDocumentEligible requires the selected command provider, not just an
|
||||
// installed guest or generic command authority. Unknown isolation fails closed.
|
||||
func sandboxDocumentEligible(c Commands) bool {
|
||||
return c.State == CommandsAvailable && c.Isolation == "isolated" && c.Addon == "sandbox" && c.Provider == "sandbox"
|
||||
// isolatedProvider reports whether the add-on serves the chat's commands and
|
||||
// the typed execution status says they are isolated. It is what the skills of
|
||||
// that add-on that need isolation require: the selected command provider, not
|
||||
// just an installed guest or generic command authority. Unknown isolation
|
||||
// fails closed.
|
||||
func isolatedProvider(c Commands, addon string) bool {
|
||||
return c.State == CommandsAvailable && c.Isolation == "isolated" && c.Addon == addon && c.Provider == addon
|
||||
}
|
||||
|
||||
// addonCommandDescriptor describes existing add-on authority without building
|
||||
|
||||
@@ -15,12 +15,16 @@ import (
|
||||
"github.com/mudler/LocalAI/core/services/studio/sandbox"
|
||||
"github.com/mudler/LocalAI/core/services/studio/studiotest"
|
||||
"github.com/mudler/LocalAI/core/services/studio/workspace"
|
||||
"github.com/mudler/LocalAI/pkg/addons/contract"
|
||||
)
|
||||
|
||||
// sandboxExecution is how the Sandbox add-on declares its commands tool.
|
||||
var sandboxExecution = &contract.ExecutionDescriptor{Class: contract.ExecutionClassCommands, Title: "Sandbox", Note: sandbox.GuestToolsNote}
|
||||
|
||||
// runAddon is the Sandbox add-on as the broker lists it: bash runs commands.
|
||||
func runAddon() []studio.AddonTool {
|
||||
bash := addonTool("sandbox", "bash")
|
||||
bash.Action, bash.Shadows = true, "bash"
|
||||
bash.Action, bash.Shadows, bash.Execution = true, "bash", sandboxExecution
|
||||
read := addonTool("sandbox", "read")
|
||||
read.Action = true
|
||||
return []studio.AddonTool{bash, read}
|
||||
@@ -76,7 +80,7 @@ var _ = Describe("capabilities", func() {
|
||||
Expect(prompt).To(ContainSubstring("do not ask a sub-agent to run it"))
|
||||
Expect(prompt).To(ContainSubstring("write the output directly with write_file"))
|
||||
Expect(prompt).To(ContainSubstring("give the user the script to run"))
|
||||
Expect(prompt).To(ContainSubstring("an administrator can enable running code by installing the Sandbox add-on"))
|
||||
Expect(prompt).To(ContainSubstring("an administrator can enable running code by installing an add-on that runs commands"))
|
||||
Expect(prompt).To(ContainSubstring("A skill marked unavailable here needs this"))
|
||||
Expect(prompt).To(ContainSubstring("- xlsx: Work with xlsx files. (unavailable here: needs command execution)"))
|
||||
Expect(prompt).NotTo(ContainSubstring("/workspace/shared-skills/xlsx/SKILL.md"), "an unavailable skill must not give the path to start it")
|
||||
@@ -106,14 +110,14 @@ var _ = Describe("capabilities", func() {
|
||||
id := create(svc, alice)
|
||||
prompt := turnPrompt(svc, h, id)
|
||||
|
||||
Expect(prompt).To(ContainSubstring("Commands: bash runs commands and code in an isolated sandbox."))
|
||||
Expect(prompt).To(ContainSubstring("Commands: bash runs commands and code in an isolated environment (Sandbox)."))
|
||||
Expect(prompt).To(ContainSubstring(sandbox.GuestToolsNote), "the isolated sandbox lists its programs")
|
||||
Expect(prompt).To(ContainSubstring("Skill folders are not visible in the Sandbox: load skills with load_skill."))
|
||||
Expect(prompt).To(ContainSubstring("Skill folders are not visible in the isolated environment: load skills with load_skill."))
|
||||
Expect(prompt).NotTo(ContainSubstring("nothing in this chat runs code"))
|
||||
Expect(prompt).To(ContainSubstring("\n- xlsx: "))
|
||||
Expect(prompt).NotTo(ContainSubstring("/workspace/shared-skills/xlsx/SKILL.md"), "the index names no path")
|
||||
Expect(prompt).NotTo(ContainSubstring("unavailable here"))
|
||||
Expect(svc.CommandRunner(context.Background(), alice, id)).To(Equal(studio.Commands{State: studio.CommandsAvailable, Tool: "bash", Isolation: "isolated", Addon: "sandbox", Provider: "sandbox"}))
|
||||
Expect(svc.CommandRunner(context.Background(), alice, id)).To(Equal(studio.Commands{State: studio.CommandsAvailable, Tool: "bash", Isolation: "isolated", Addon: "sandbox", Provider: "sandbox", Title: "Sandbox", Note: sandbox.GuestToolsNote}))
|
||||
})
|
||||
|
||||
It("warns when commands run on the server with no isolation", func() {
|
||||
@@ -133,7 +137,7 @@ var _ = Describe("capabilities", func() {
|
||||
prompt := turnPrompt(svc, h, id)
|
||||
Expect(prompt).To(ContainSubstring("nothing in this chat runs code or commands"))
|
||||
Expect(prompt).To(ContainSubstring("they can switch the Sandbox add-on on in the Tools menu of this chat"))
|
||||
Expect(svc.CommandRunner(context.Background(), alice, id)).To(Equal(studio.Commands{State: studio.CommandsOff, Addon: "sandbox"}))
|
||||
Expect(svc.CommandRunner(context.Background(), alice, id)).To(Equal(studio.Commands{State: studio.CommandsOff, Addon: "sandbox", Title: "Sandbox"}))
|
||||
})
|
||||
|
||||
It("says the Sandbox cannot run when no backend works", func() {
|
||||
@@ -143,7 +147,7 @@ var _ = Describe("capabilities", func() {
|
||||
id := create(svc, alice)
|
||||
prompt := turnPrompt(svc, h, id)
|
||||
Expect(prompt).To(ContainSubstring("the Sandbox add-on cannot run commands on this server"))
|
||||
Expect(svc.CommandRunner(context.Background(), alice, id)).To(Equal(studio.Commands{State: studio.CommandsUnavailable, Reason: "no sandbox backend works on this host", Addon: "sandbox"}))
|
||||
Expect(svc.CommandRunner(context.Background(), alice, id)).To(Equal(studio.Commands{State: studio.CommandsUnavailable, Reason: "no sandbox backend works on this host", Addon: "sandbox", Title: "Sandbox"}))
|
||||
})
|
||||
|
||||
It("does not call the add-on off for a user who cannot use it", func() {
|
||||
@@ -278,3 +282,44 @@ var _ = Describe("execution presentation DTO", func() {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("a second provider of commands", func() {
|
||||
runbox := func() []studio.AddonTool {
|
||||
bash := addonTool("runbox", "bash")
|
||||
bash.Action, bash.Shadows = true, "bash"
|
||||
bash.Execution = &contract.ExecutionDescriptor{Class: contract.ExecutionClassCommands, Title: "Runbox", Note: "Runbox has python3 and jq."}
|
||||
return []studio.AddonTool{bash}
|
||||
}
|
||||
It("is described by the title and the note it declares, not by a name known to the host", func() {
|
||||
fa := &fakeAddons{tools: map[string][]studio.AddonTool{alice: runbox()}}
|
||||
svc, h, _ := newCapsChat(isolatedAddons{fakeAddons: fa, mode: "isolated"}, nil)
|
||||
id := create(svc, alice)
|
||||
prompt := turnPrompt(svc, h, id)
|
||||
Expect(prompt).To(ContainSubstring("Commands: bash runs commands and code in an isolated environment (Runbox)."))
|
||||
Expect(prompt).To(ContainSubstring("Runbox has python3 and jq."))
|
||||
Expect(prompt).NotTo(ContainSubstring("Sandbox"))
|
||||
Expect(svc.CommandRunner(context.Background(), alice, id)).To(Equal(studio.Commands{State: studio.CommandsAvailable, Tool: "bash", Isolation: "isolated", Addon: "runbox", Provider: "runbox", Title: "Runbox", Note: "Runbox has python3 and jq."}))
|
||||
})
|
||||
It("is named when the chat switched it off and when it cannot run", func() {
|
||||
fa := &fakeAddons{tools: map[string][]studio.AddonTool{alice: runbox()}}
|
||||
svc, h, _ := newCapsChat(fa, nil)
|
||||
id := create(svc, alice)
|
||||
Expect(svc.SetAddonsOff(context.Background(), alice, id, []string{"runbox"})).To(Succeed())
|
||||
Expect(turnPrompt(svc, h, id)).To(ContainSubstring("they can switch the Runbox add-on on in the Tools menu of this chat"))
|
||||
|
||||
gate := gatedAddons{fakeAddons: fa, reasons: map[string]string{"runbox": "no backend works"}}
|
||||
svc, h, _ = newCapsChat(gate, nil)
|
||||
id = create(svc, alice)
|
||||
prompt := turnPrompt(svc, h, id)
|
||||
Expect(prompt).To(ContainSubstring("the Runbox add-on cannot run commands on this server"))
|
||||
Expect(prompt).NotTo(ContainSubstring("Sandbox"))
|
||||
})
|
||||
It("behaves as an unnamed provider when its manifest declares no descriptor", func() {
|
||||
bash := addonTool("runbox", "bash")
|
||||
bash.Action, bash.Shadows = true, "bash"
|
||||
fa := &fakeAddons{tools: map[string][]studio.AddonTool{alice: {bash}}}
|
||||
svc, h, _ := newCapsChat(isolatedAddons{fakeAddons: fa, mode: ""}, nil)
|
||||
id := create(svc, alice)
|
||||
Expect(turnPrompt(svc, h, id)).To(ContainSubstring("Commands: bash runs commands and code."))
|
||||
})
|
||||
})
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/mudler/LocalAI/core/services/studio/studiotest"
|
||||
|
||||
"github.com/mudler/LocalAI/core/services/studio/skills"
|
||||
"github.com/mudler/LocalAI/pkg/addons/contract"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
@@ -23,7 +24,7 @@ var _ = Describe("document command eligibility", func() {
|
||||
b := &skillBroker{}
|
||||
svc, f := newSkillService(b)
|
||||
Expect(f.SyncAddonSkills(ctx, "sandbox", fstest.MapFS{"docx/SKILL.md": {Data: md("docx", "Sandbox document.")}})).To(Succeed())
|
||||
b.set(AddonSkillSet{AddonID: "sandbox", Skills: []string{"docx"}, Shadows: []string{"docx"}, Running: true})
|
||||
b.set(AddonSkillSet{AddonID: "sandbox", Skills: []string{"docx"}, Shadows: []string{"docx"}, Isolated: []string{"docx"}, Running: true})
|
||||
list, _, err := svc.Skills(ctx, "alice", "")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(skills.Names(skills.Usable(list))).NotTo(ContainElement("docx"))
|
||||
@@ -37,21 +38,44 @@ var _ = Describe("document command eligibility", func() {
|
||||
"pdf/SKILL.md": {Data: md("pdf", "Sandbox pdf.")},
|
||||
"plot/SKILL.md": {Data: md("plot", "Sandbox plots.")},
|
||||
})).To(Succeed())
|
||||
b.set(AddonSkillSet{AddonID: "sandbox", Skills: []string{"docx", "pdf", "plot"}, Shadows: []string{"docx", "pdf"}, Running: true})
|
||||
b.set(AddonSkillSet{AddonID: "sandbox", Skills: []string{"docx", "pdf", "plot"}, Shadows: []string{"docx", "pdf"}, Isolated: []string{"docx", "pdf"}, Running: true})
|
||||
_, notes, err := chatSkillNotes(svc, ctx, off, command)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
if wantNote {
|
||||
Expect(notes).To(ContainElement(HavePrefix("the document skills docx, pdf need an isolated Sandbox; ")))
|
||||
Expect(notes).To(ContainElement(HavePrefix("the document skills docx, pdf need isolated command execution by Sandbox; ")))
|
||||
} else {
|
||||
Expect(strings.Join(notes, ";")).NotTo(ContainSubstring("isolated Sandbox"))
|
||||
Expect(strings.Join(notes, ";")).NotTo(ContainSubstring("isolated command execution"))
|
||||
}
|
||||
},
|
||||
Entry("commands run on the host", Commands{State: CommandsAvailable, Addon: "sandbox", Provider: "sandbox", Isolation: "host"}, nil, true),
|
||||
Entry("isolation unknown", Commands{State: CommandsAvailable, Addon: "sandbox", Provider: "sandbox"}, nil, true),
|
||||
Entry("commands run on the host", Commands{State: CommandsAvailable, Addon: "sandbox", Provider: "sandbox", Title: "Sandbox", Isolation: "host"}, nil, true),
|
||||
Entry("isolation unknown", Commands{State: CommandsAvailable, Addon: "sandbox", Provider: "sandbox", Title: "Sandbox"}, nil, true),
|
||||
Entry("isolated", Commands{State: CommandsAvailable, Addon: "sandbox", Provider: "sandbox", Isolation: "isolated"}, nil, false),
|
||||
Entry("another provider runs the commands", Commands{State: CommandsAvailable, Addon: "other", Provider: "other", Isolation: "host"}, nil, false),
|
||||
Entry("the Sandbox is off for the chat", Commands{State: CommandsAvailable, Addon: "sandbox", Provider: "sandbox", Isolation: "host"}, []string{"sandbox"}, false),
|
||||
)
|
||||
It("names the second provider by the title it declares, in the note and in the commands", func() {
|
||||
ctx := context.Background()
|
||||
b := &documentBroker{skillBroker: &skillBroker{}, isolation: "host", defs: []AddonTool{{AddonID: "runbox", Name: "addon__runbox__exec", Shadows: "bash",
|
||||
Execution: &contract.ExecutionDescriptor{Class: contract.ExecutionClassCommands, Title: "Runbox"}}}}
|
||||
svc, f := newSkillService(b.skillBroker)
|
||||
svc.d.AddonTools = b
|
||||
Expect(f.SyncAddonSkills(ctx, "runbox", fstest.MapFS{"docx/SKILL.md": {Data: md("docx", "Runbox document.")}})).To(Succeed())
|
||||
b.set(AddonSkillSet{AddonID: "runbox", Skills: []string{"docx"}, Shadows: []string{"docx"}, Isolated: []string{"docx"}, Running: true})
|
||||
rec, err := svc.Create(ctx, "alice", CreateInput{Model: "m"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
command := documentCommands(svc, ctx, "alice", rec, nil)
|
||||
Expect(command.Title).To(Equal("Runbox"))
|
||||
Expect(isolatedProvider(command, "runbox")).To(BeFalse(), "the status says host")
|
||||
_, notes, err := chatSkillNotes(svc, ctx, nil, command)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(notes).To(ContainElement(ContainSubstring("need isolated command execution by Runbox; its commands run on the server without isolation")))
|
||||
b.isolation = "isolated"
|
||||
command = documentCommands(svc, ctx, "alice", rec, nil)
|
||||
Expect(isolatedProvider(command, "runbox")).To(BeTrue())
|
||||
list, _, err := chatSkillNotes(svc, ctx, nil, command)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(skills.Names(skills.Usable(list))).To(ContainElement("docx"))
|
||||
})
|
||||
It("does not switch a missing pinned document provider to Office", func() {
|
||||
a := addonSkills{pins: map[string]string{"docx": "sandbox"}, usable: []AddonSkillSet{{AddonID: "office", Shadows: []string{"docx"}}}}
|
||||
id, chosen := a.provider("docx")
|
||||
@@ -60,6 +84,9 @@ var _ = Describe("document command eligibility", func() {
|
||||
})
|
||||
})
|
||||
|
||||
// sandboxExecution is how the Sandbox add-on declares its commands tool.
|
||||
var sandboxExecution = &contract.ExecutionDescriptor{Class: contract.ExecutionClassCommands, Title: "Sandbox"}
|
||||
|
||||
// This broker lists metadata but fails immediately if descriptor construction
|
||||
// invokes a command. Keeping it separate avoids changing older fixtures.
|
||||
type documentBroker struct {
|
||||
@@ -88,7 +115,11 @@ var _ = Describe("document provider matrix", func() {
|
||||
svc, f := newSkillService(b, map[string]string{"docx": pin})
|
||||
for _, id := range ids {
|
||||
Expect(f.SyncAddonSkills(ctx, id, fstest.MapFS{"docx/SKILL.md": {Data: md("docx", id+" document.")}})).To(Succeed())
|
||||
b.sets = append(b.sets, AddonSkillSet{AddonID: id, Skills: []string{"docx"}, Shadows: []string{"docx"}, Running: true})
|
||||
set := AddonSkillSet{AddonID: id, Skills: []string{"docx"}, Shadows: []string{"docx"}, Running: true}
|
||||
if id == "sandbox" || id == "runbox" { // office ships no commands tool, so its document skill needs no isolation
|
||||
set.Isolated = []string{"docx"}
|
||||
}
|
||||
b.sets = append(b.sets, set)
|
||||
}
|
||||
list, _, _, err := svc.chatSkillSet(ctx, "alice", nil, off, sc, command)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
@@ -114,34 +145,38 @@ var _ = Describe("document provider matrix", func() {
|
||||
Entry("unavailable pin", []string{"sandbox", "office"}, Commands{}, nil, nil, "sandbox", ""),
|
||||
Entry("missing pin", []string{"office"}, Commands{}, nil, nil, "sandbox", ""),
|
||||
Entry("eligible pin", []string{"sandbox", "office"}, Commands{State: CommandsAvailable, Addon: "sandbox", Provider: "sandbox", Isolation: "isolated"}, nil, nil, "sandbox", "sandbox"),
|
||||
Entry("a second provider selected", []string{"runbox"}, Commands{State: CommandsAvailable, Addon: "runbox", Provider: "runbox", Isolation: "isolated"}, nil, nil, "", "runbox"),
|
||||
Entry("a second provider on the host", []string{"runbox"}, Commands{State: CommandsAvailable, Addon: "runbox", Provider: "runbox", Isolation: "host"}, nil, nil, "", ""),
|
||||
Entry("a second provider with unknown isolation", []string{"runbox"}, Commands{State: CommandsAvailable, Addon: "runbox", Provider: "runbox"}, nil, nil, "", ""),
|
||||
Entry("the other provider runs the commands", []string{"runbox"}, Commands{State: CommandsAvailable, Addon: "sandbox", Provider: "sandbox", Isolation: "isolated"}, nil, nil, "", ""),
|
||||
Entry("existing default tie", []string{"sandbox", "office"}, Commands{State: CommandsAvailable, Addon: "sandbox", Provider: "sandbox", Isolation: "isolated"}, nil, nil, "", "office"),
|
||||
)
|
||||
It("describes selected commands without recursive tool building or execution, including subagent final pruning", func() {
|
||||
ctx := context.Background()
|
||||
svc, _ := newSkillService(&skillBroker{})
|
||||
b := &documentBroker{skillBroker: &skillBroker{}, isolation: "isolated", defs: []AddonTool{{AddonID: "sandbox", Name: "addon__sandbox__exec", Shadows: "bash", Schema: json.RawMessage(`{"type":"object"}`)}}}
|
||||
b := &documentBroker{skillBroker: &skillBroker{}, isolation: "isolated", defs: []AddonTool{{AddonID: "sandbox", Name: "addon__sandbox__exec", Shadows: "bash", Execution: sandboxExecution, Schema: json.RawMessage(`{"type":"object"}`)}}}
|
||||
svc.d.AddonTools = b
|
||||
rec, err := svc.Create(ctx, "alice", CreateInput{Model: "m"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
c := documentCommands(svc, ctx, "alice", rec, nil)
|
||||
Expect(sandboxDocumentEligible(c)).To(BeTrue())
|
||||
Expect(isolatedProvider(c, "sandbox")).To(BeTrue())
|
||||
Expect(b.calls).To(BeNumerically("<=", 3))
|
||||
rec.Kind = session.KindSubAgent
|
||||
Expect(sandboxDocumentEligible(documentCommands(svc, ctx, "alice", rec, nil))).To(BeFalse())
|
||||
Expect(isolatedProvider(documentCommands(svc, ctx, "alice", rec, nil), "sandbox")).To(BeFalse())
|
||||
svc.allowed[key("alice", rec.ID)] = []session.Tool{{Name: "bash", Provider: "sandbox"}}
|
||||
Expect(sandboxDocumentEligible(documentCommands(svc, ctx, "alice", rec, nil))).To(BeTrue())
|
||||
Expect(sandboxDocumentEligible(documentCommands(svc, ctx, "alice", rec, &scope{deleted: true}))).To(BeFalse())
|
||||
Expect(isolatedProvider(documentCommands(svc, ctx, "alice", rec, nil), "sandbox")).To(BeTrue())
|
||||
Expect(isolatedProvider(documentCommands(svc, ctx, "alice", rec, &scope{deleted: true}), "sandbox")).To(BeFalse())
|
||||
})
|
||||
DescribeTable("uses saved-agent subagent grants for the index and loader", func(granted bool) {
|
||||
ctx := context.Background()
|
||||
b := &documentBroker{skillBroker: &skillBroker{}, isolation: "isolated", defs: []AddonTool{{AddonID: "sandbox", Name: "addon__sandbox__exec", Shadows: "bash"}}}
|
||||
b := &documentBroker{skillBroker: &skillBroker{}, isolation: "isolated", defs: []AddonTool{{AddonID: "sandbox", Name: "addon__sandbox__exec", Shadows: "bash", Execution: sandboxExecution}}}
|
||||
svc, f := newSkillService(b.skillBroker)
|
||||
svc.d.AddonTools = b
|
||||
agentsStore, err := agents.NewJSON("")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
svc.d.Agents = agentsStore
|
||||
Expect(f.SyncAddonSkills(ctx, "sandbox", fstest.MapFS{"docx/SKILL.md": {Data: skillMD("docx", "Sandbox document.", "Sandbox body.", "requires: [exec]")}})).To(Succeed())
|
||||
b.set(AddonSkillSet{AddonID: "sandbox", Skills: []string{"docx"}, Shadows: []string{"docx"}, Running: true})
|
||||
b.set(AddonSkillSet{AddonID: "sandbox", Skills: []string{"docx"}, Shadows: []string{"docx"}, Isolated: []string{"docx"}, Running: true})
|
||||
grants := []string{}
|
||||
if granted {
|
||||
grants = []string{"sandbox"}
|
||||
@@ -200,14 +235,14 @@ var _ = Describe("document provider matrix", func() {
|
||||
|
||||
It("says why a document skill is missing when the status cannot be read after the tool was offered", func() {
|
||||
ctx := context.Background()
|
||||
b := &documentBroker{skillBroker: &skillBroker{}, isolation: "isolated", defs: []AddonTool{{AddonID: "sandbox", Name: "addon__sandbox__exec", Shadows: "bash"}}}
|
||||
b := &documentBroker{skillBroker: &skillBroker{}, isolation: "isolated", defs: []AddonTool{{AddonID: "sandbox", Name: "addon__sandbox__exec", Shadows: "bash", Execution: sandboxExecution}}}
|
||||
svc, f := newSkillService(b.skillBroker)
|
||||
svc.d.AddonTools = b
|
||||
agentsStore, err := agents.NewJSON("")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
svc.d.Agents = agentsStore
|
||||
Expect(f.SyncAddonSkills(ctx, "sandbox", fstest.MapFS{"pptx/SKILL.md": {Data: skillMD("pptx", "Sandbox slides.", "Slides body.", "requires: [exec]")}})).To(Succeed())
|
||||
b.set(AddonSkillSet{AddonID: "sandbox", Skills: []string{"pptx"}, Shadows: []string{"pptx"}, Running: true})
|
||||
b.set(AddonSkillSet{AddonID: "sandbox", Skills: []string{"pptx"}, Shadows: []string{"pptx"}, Isolated: []string{"pptx"}, Running: true})
|
||||
agent, err := svc.SaveAgent(ctx, "alice", agents.Agent{Name: "Slides", Model: "m", Addons: []string{"sandbox"}, Skills: []string{"pptx"}})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
parent, err := svc.Create(ctx, "alice", CreateInput{Model: "m"})
|
||||
@@ -230,7 +265,7 @@ var _ = Describe("document provider matrix", func() {
|
||||
_, err = loader.Run(ctx, json.RawMessage(`{"name":"pptx"}`))
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(err.Error()).NotTo(ContainSubstring("has no skills now"))
|
||||
Expect(err.Error()).To(ContainSubstring("need an isolated Sandbox; its status could not be read"))
|
||||
Expect(err.Error()).To(ContainSubstring("need isolated command execution by Sandbox; its status could not be read"))
|
||||
})
|
||||
It("opens a scoped chat with load_skill without reentering session access resolution", func() {
|
||||
ctx := context.Background()
|
||||
@@ -276,30 +311,30 @@ var _ = Describe("document provider matrix", func() {
|
||||
It("refuses another user's chat and access errors rather than using the whole home", func() {
|
||||
ctx := context.Background()
|
||||
svc, _ := newSkillService(&skillBroker{})
|
||||
b := &documentBroker{skillBroker: &skillBroker{}, isolation: "isolated", defs: []AddonTool{{AddonID: "sandbox", Name: "addon__sandbox__exec", Shadows: "bash"}}}
|
||||
b := &documentBroker{skillBroker: &skillBroker{}, isolation: "isolated", defs: []AddonTool{{AddonID: "sandbox", Name: "addon__sandbox__exec", Shadows: "bash", Execution: sandboxExecution}}}
|
||||
svc.d.AddonTools = b
|
||||
rec, err := svc.Create(ctx, "alice", CreateInput{Model: "m"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
c, err := svc.addonCommandDescriptor(ctx, "bob", rec, nil)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(sandboxDocumentEligible(c)).To(BeFalse())
|
||||
Expect(isolatedProvider(c, "sandbox")).To(BeFalse())
|
||||
Expect(b.calls).To(BeZero())
|
||||
rec.ID = "missing"
|
||||
c, err = svc.addonCommandDescriptor(ctx, "alice", rec, nil)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(sandboxDocumentEligible(c)).To(BeFalse())
|
||||
Expect(isolatedProvider(c, "sandbox")).To(BeFalse())
|
||||
Expect(b.calls).To(BeZero())
|
||||
// Only the genuinely chatless picker may describe person capabilities.
|
||||
Expect(sandboxDocumentEligible(documentCommands(svc, ctx, "alice", session.ChatRecord{}, nil))).To(BeTrue())
|
||||
Expect(isolatedProvider(documentCommands(svc, ctx, "alice", session.ChatRecord{}, nil), "sandbox")).To(BeTrue())
|
||||
})
|
||||
|
||||
It("keeps picker, slash selection, index and fresh loads aligned across readiness and final pruning", func() {
|
||||
ctx := context.Background()
|
||||
b := &documentBroker{skillBroker: &skillBroker{}, isolation: "isolated", defs: []AddonTool{{AddonID: "sandbox", Name: "addon__sandbox__exec", Shadows: "bash"}}}
|
||||
b := &documentBroker{skillBroker: &skillBroker{}, isolation: "isolated", defs: []AddonTool{{AddonID: "sandbox", Name: "addon__sandbox__exec", Shadows: "bash", Execution: sandboxExecution}}}
|
||||
svc, f := newSkillService(b.skillBroker)
|
||||
svc.d.AddonTools = b
|
||||
Expect(f.SyncAddonSkills(ctx, "sandbox", fstest.MapFS{"docx/SKILL.md": {Data: skillMD("docx", "Sandbox document.", "Sandbox body.", "requires: [exec]")}})).To(Succeed())
|
||||
b.set(AddonSkillSet{AddonID: "sandbox", Skills: []string{"docx"}, Shadows: []string{"docx"}, Running: true})
|
||||
b.set(AddonSkillSet{AddonID: "sandbox", Skills: []string{"docx"}, Shadows: []string{"docx"}, Isolated: []string{"docx"}, Running: true})
|
||||
rec, err := svc.Create(ctx, "alice", CreateInput{Model: "m"})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
a := userChatActor("alice", rec.ID)
|
||||
@@ -341,7 +376,7 @@ var _ = Describe("document provider matrix", func() {
|
||||
svc.d.ToolPins = func(context.Context) (map[string]string, error) { return nil, unavailable }
|
||||
command, err := svc.addonCommandDescriptor(ctx, "alice", rec, nil)
|
||||
Expect(errors.Is(err, unavailable)).To(BeTrue())
|
||||
Expect(sandboxDocumentEligible(command)).To(BeFalse())
|
||||
Expect(isolatedProvider(command, "sandbox")).To(BeFalse())
|
||||
_, err = svc.CommandRunner(ctx, "alice", rec.ID)
|
||||
Expect(errors.Is(err, unavailable)).To(BeTrue())
|
||||
_, _, err = svc.Skills(ctx, "alice", rec.ID)
|
||||
|
||||
@@ -43,8 +43,12 @@ type loadOpts struct {
|
||||
files bool
|
||||
// exec is true when the chat can run commands. A skill that requires
|
||||
// them is refused with the reason when it cannot.
|
||||
exec bool
|
||||
sandbox *bool
|
||||
exec bool
|
||||
// provided is true when an add-on, not the built-in bash, serves the
|
||||
// chat's commands in the final tool list. Without it the commands count as
|
||||
// absent when the skills resolve, so a skill that needs an add-on's
|
||||
// isolation is not loaded on the strength of a stale descriptor.
|
||||
provided *bool
|
||||
}
|
||||
|
||||
// registerLoadSkill adds load_skill to the registry of built-in tools. It has
|
||||
@@ -96,8 +100,8 @@ func (s *Service) fitLoadSkill(a workspace.Actor, tools []session.Tool) []sessio
|
||||
return tools
|
||||
}
|
||||
// Final tools may only narrow the freshly checked descriptor at call time.
|
||||
sandbox := addonRunner(tools) == "sandbox"
|
||||
tools[i].Run = s.loadSkillRun(a, loadOpts{files: hasTool(tools, "read_file"), exec: s.runsCommands(tools), sandbox: &sandbox})
|
||||
provided := addonRunner(tools) != ""
|
||||
tools[i].Run = s.loadSkillRun(a, loadOpts{files: hasTool(tools, "read_file"), exec: s.runsCommands(tools), provided: &provided})
|
||||
return tools
|
||||
}
|
||||
|
||||
@@ -160,12 +164,12 @@ func (s *Service) loadSkillRun(a workspace.Actor, o loadOpts) func(context.Conte
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
list, notes, err := s.loadableSkillsAndNotes(ctx, a, o.sandbox)
|
||||
list, notes, err := s.loadableSkillsAndNotes(ctx, a, o.provided)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("the skills cannot be read now: %w", err)
|
||||
}
|
||||
// The tool was offered because the chat had this skill; a document
|
||||
// skill is missing now only because the Sandbox is not known to be
|
||||
// skill is missing now only because its add-on is not known to be
|
||||
// isolated, and the model must say so rather than "no skills".
|
||||
if _, ok := skills.Find(list, in.Name); !ok && contract.DocumentSkill(skills.Clean(in.Name)) {
|
||||
if i := slices.IndexFunc(notes, func(n string) bool { return strings.HasPrefix(n, documentNotePrefix) }); i >= 0 {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package studio_test
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// Core decides what an add-on may do from what the add-on declares and what it
|
||||
// reports at run time. These specs fail when a file that sets the rules for
|
||||
// command execution names one add-on again. Tests, comments, documentation and
|
||||
// the add-on's own data are the places that may.
|
||||
var _ = Describe("the command execution code of the host", func() {
|
||||
// literals returns the string literals of the non-test Go files of dir.
|
||||
literals := func(dir string, only ...string) map[string][]string {
|
||||
out := map[string][]string{}
|
||||
entries, err := os.ReadDir(dir)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if e.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
|
||||
continue
|
||||
}
|
||||
if len(only) > 0 && !slices.Contains(only, name) {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(dir, name)
|
||||
file, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
if lit, ok := n.(*ast.BasicLit); ok && lit.Kind == token.STRING {
|
||||
v, err := strconv.Unquote(lit.Value)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
out[path] = append(out[path], v)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
It("compares no string with the ID of the add-on that runs commands", func() {
|
||||
// Other add-on IDs join this list as their special cases are removed.
|
||||
ids := []string{"sandbox"}
|
||||
for _, dir := range []string{".", "../addons/tools"} {
|
||||
for path, values := range literals(dir) {
|
||||
for _, v := range values {
|
||||
Expect(ids).NotTo(ContainElement(v), "%s compares with the ID of an add-on", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
It("names no add-on in the text it builds for the commands of a chat", func() {
|
||||
for path, values := range literals(".", "capabilities.go", "addonskills.go", "loadskill.go", "shadow.go") {
|
||||
for _, v := range values {
|
||||
Expect(strings.ToLower(v)).NotTo(ContainSubstring("sandbox"), "%s writes a provider name into a sentence; take it from the manifest", path)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -3,7 +3,9 @@ package session
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mudler/LocalAI/core/services/studio/workspace"
|
||||
"github.com/mudler/LocalAI/pkg/addons/contract"
|
||||
)
|
||||
|
||||
// Harness starts agent sessions.
|
||||
@@ -78,6 +80,9 @@ type Tool struct {
|
||||
// say): "builtin" or the add-on's ID. It is empty for every other tool.
|
||||
// The harness does not read it.
|
||||
Provider string
|
||||
// Execution is what the add-on that serves a command tool declares about
|
||||
// itself, or nil. The harness does not read it.
|
||||
Execution *contract.ExecutionDescriptor
|
||||
// StandInFor is set on the stand-in tool of an add-on that cannot work
|
||||
// now: the built-in tools that add-on replaces (bash, say), so the chat
|
||||
// can tell that the built-in serves in its place. The harness does not
|
||||
|
||||
@@ -56,7 +56,7 @@ func (s *Service) shadowTool(a workspace.Actor, d AddonTool, schema json.RawMess
|
||||
// What the command saved in the user's files goes on its card.
|
||||
run = s.withCommandFiles(a, run)
|
||||
}
|
||||
return session.Tool{Name: d.Shadows, Description: d.Description, Schema: schema, Provider: addon, Run: run}, true
|
||||
return session.Tool{Name: d.Shadows, Description: d.Description, Schema: schema, Provider: addon, Execution: d.Execution, Run: run}, true
|
||||
}
|
||||
|
||||
// withBuiltins adds the built-in tools that no add-on shadows. A built-in whose
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// bash, which shadows the built-in bash, and status under its add-on name.
|
||||
func shadowingSandbox() []studio.AddonTool {
|
||||
bash := addonTool("sandbox", "bash")
|
||||
bash.Action, bash.Shadows = true, "bash"
|
||||
bash.Action, bash.Shadows, bash.Execution = true, "bash", sandboxExecution
|
||||
return []studio.AddonTool{bash, addonTool("sandbox", "status")}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,6 @@ var _ = Describe("an add-on tool that shadows a built-in", func() {
|
||||
fa := &fakeAddons{tools: map[string][]studio.AddonTool{alice: shadowingSandbox()}}
|
||||
svc, h, _ := newCapsChat(isolatedAddons{fa, "isolated"}, nil)
|
||||
id := create(svc, alice)
|
||||
Expect(turnPrompt(svc, h, id)).To(ContainSubstring("Commands: bash runs commands and code in an isolated sandbox."))
|
||||
Expect(turnPrompt(svc, h, id)).To(ContainSubstring("Commands: bash runs commands and code in an isolated environment (Sandbox)."))
|
||||
})
|
||||
})
|
||||
@@ -139,7 +139,8 @@ and reconciling the operation before further use.
|
||||
## Optional typed manifest extensions
|
||||
|
||||
These declarations extend the strict manifest contract. They keep
|
||||
`schemaVersion: 1`, host/API/protocol/data-format level 1 and UI SDK 2.
|
||||
`schemaVersion: 1`, host/protocol/data-format level 1 and UI SDK 2. The manifest
|
||||
`api` range is 1 to 2: only the execution descriptor below needs 2.
|
||||
The Go contract parser is authoritative; there is no separate JSON Schema.
|
||||
The fragments below merge into an otherwise valid manifest, not a complete
|
||||
installable package. Existing executable, identity, compatibility and UI
|
||||
@@ -287,6 +288,53 @@ list and rejects its pending responses, even before effect cleanup. A new list
|
||||
request can populate the refreshed session. These in-memory scopes contain no
|
||||
credentials.
|
||||
|
||||
### Execution descriptor of a commands tool
|
||||
|
||||
Needs manifest `api` 2. On the definition of a tool that shadows `bash`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "bash",
|
||||
"kind": "action",
|
||||
"shadows": "bash",
|
||||
"description": "...",
|
||||
"inputSchema": { "...": "..." },
|
||||
"execution": {
|
||||
"class": "commands",
|
||||
"title": "Runbox",
|
||||
"note": "The environment has python3, jq and sqlite3.",
|
||||
"skills": { "providesRuntimeFor": ["documents"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The descriptor says what the add-on is, so that the host can write about it
|
||||
without knowing its ID. It grants no authority. Whether commands are ready and
|
||||
isolated stays what the provider reports in its execution status (see above);
|
||||
the host decides from that report.
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `class` | Required. `commands`: the tool runs shell commands and code. |
|
||||
| `title` | Optional, at most 40 characters of plain text. The host calls the provider by this name in the prompt and in the Tools menu text ("Runbox runs commands in an isolated environment", "switch the Runbox add-on on"). Without it, the host uses the add-on's own title. |
|
||||
| `note` | Optional, at most 600 characters of plain text. One sentence for the model, such as the programs the environment has. The host adds it to the Commands block only while the provider reports `isolated`. |
|
||||
| `skills.providesRuntimeFor` | Optional list of skill kinds. `documents` means the add-on's document skills (`xlsx`, `docx`, `pptx`, `pdf`) run on this provider. |
|
||||
|
||||
The host offers the document skills of an add-on only while that add-on serves
|
||||
the chat's commands and its status reports `isolated`. When the status is
|
||||
`host` or unknown, or another provider serves the commands, the skills are left
|
||||
out and the chat says why. A tool descriptor may leave `providesRuntimeFor`
|
||||
out to opt out of that check, because its skills do not need its commands.
|
||||
|
||||
The field is optional and does not change the protocol. A manifest that
|
||||
declares a descriptor must accept API 2 (`"api": {"min": 1, "max": 2}`). The
|
||||
host speaks API 2 and still accepts add-ons that declare `{1,1}`. An add-on
|
||||
whose commands tool declares no descriptor works as before: the host treats it
|
||||
as a provider of commands under the add-on's own title, with no note, and keeps
|
||||
the isolation check for its document skills. The host never compares the add-on
|
||||
ID with a fixed name for any of this, so a second add-on that runs commands gets
|
||||
the same behavior with its own title.
|
||||
|
||||
### Requested host operations are not grants
|
||||
|
||||
Inside `capabilities`:
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package contract
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// MinExecutionAPI is the first manifest API version that knows the execution
|
||||
// descriptor. A manifest that declares one must accept it.
|
||||
const MinExecutionAPI uint64 = 2
|
||||
|
||||
// ExecutionClassCommands is the class of a tool that runs shell commands and
|
||||
// code: the tool that shadows bash.
|
||||
const ExecutionClassCommands = "commands"
|
||||
|
||||
// Kinds of skills a commands provider can supply the runtime for.
|
||||
const (
|
||||
// SkillKindDocuments are the skills of the document slots (see
|
||||
// DocumentSkill). They need a provider that reports isolated execution.
|
||||
SkillKindDocuments = "documents"
|
||||
)
|
||||
|
||||
// Limits of the text the host shows for an execution provider.
|
||||
const (
|
||||
MaxExecutionTitleChars = 40
|
||||
MaxExecutionNoteChars = 600
|
||||
)
|
||||
|
||||
// ExecutionDescriptor is what an add-on declares about the tool that runs
|
||||
// commands. It carries no authority. Whether commands are ready and isolated
|
||||
// is what the provider reports at run time (ExecutionStatus); the host decides
|
||||
// from that report and never from the add-on's ID.
|
||||
type ExecutionDescriptor struct {
|
||||
// Class is ExecutionClassCommands.
|
||||
Class string `json:"class"`
|
||||
// Title names the provider in the text the host shows the person and the
|
||||
// model, for example "Sandbox". It falls back to the add-on's title.
|
||||
Title string `json:"title,omitempty"`
|
||||
// Note is one plain sentence for the model when the provider reports
|
||||
// isolated execution, such as the programs its environment has.
|
||||
Note string `json:"note,omitempty"`
|
||||
// Skills says which skills run on this provider.
|
||||
Skills *ExecutionSkills `json:"skills,omitempty"`
|
||||
}
|
||||
|
||||
// ExecutionSkills declares the skills whose runtime the provider supplies.
|
||||
type ExecutionSkills struct {
|
||||
// ProvidesRuntimeFor lists skill kinds (SkillKindDocuments). The host
|
||||
// offers the add-on's skills of those kinds only while the provider
|
||||
// reports isolated execution.
|
||||
ProvidesRuntimeFor []string `json:"providesRuntimeFor,omitempty"`
|
||||
}
|
||||
|
||||
// validate checks the descriptor of tool d. A nil descriptor is valid.
|
||||
func (e *ExecutionDescriptor) validate(d ToolDefinition) error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
if e.Class != ExecutionClassCommands || d.Shadows != "bash" {
|
||||
return fmt.Errorf("tool %s: an execution descriptor has class %q and belongs to a tool that shadows bash", d.Name, ExecutionClassCommands)
|
||||
}
|
||||
if e.Title != "" && !plainText(e.Title, MaxExecutionTitleChars) {
|
||||
return fmt.Errorf("tool %s: invalid execution title", d.Name)
|
||||
}
|
||||
if e.Note != "" && !plainText(e.Note, MaxExecutionNoteChars) {
|
||||
return fmt.Errorf("tool %s: invalid execution note", d.Name)
|
||||
}
|
||||
if e.Skills != nil {
|
||||
for _, k := range e.Skills.ProvidesRuntimeFor {
|
||||
if k != SkillKindDocuments {
|
||||
return fmt.Errorf("tool %s: unknown skill kind %q", d.Name, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommandsTool returns the tool of the definitions that shadows bash.
|
||||
func CommandsTool(defs []ToolDefinition) (ToolDefinition, bool) {
|
||||
i := slices.IndexFunc(defs, func(d ToolDefinition) bool { return d.Shadows == "bash" && ValidateShadow(d) == nil })
|
||||
if i < 0 {
|
||||
return ToolDefinition{}, false
|
||||
}
|
||||
return defs[i], true
|
||||
}
|
||||
|
||||
// IsolatedSkills lists the skills of an add-on that need its own commands
|
||||
// provider to report isolated execution: the document-slot skills of an
|
||||
// add-on with a commands tool, unless its descriptor says it does not provide
|
||||
// the runtime for documents. A commands tool without a descriptor (a manifest
|
||||
// of API 1) counts as providing it, so such an add-on keeps the check.
|
||||
func IsolatedSkills(defs []ToolDefinition, skills []SkillDefinition) []string {
|
||||
tool, ok := CommandsTool(defs)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if e := tool.Execution; e != nil && (e.Skills == nil || !slices.Contains(e.Skills.ProvidesRuntimeFor, SkillKindDocuments)) {
|
||||
return nil
|
||||
}
|
||||
return ShadowedSkills(skills)
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package contract
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("The execution descriptor of a commands tool", func() {
|
||||
const schema = `{"type":"object","properties":{"command":{"type":"string"}},"required":["command"],"additionalProperties":false}`
|
||||
manifestWith := func(e *ExecutionDescriptor, api VersionRange) Manifest {
|
||||
m, err := ParseManifest(fixture())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m.API = api
|
||||
m.Capabilities = Capabilities{
|
||||
Tools: []string{"run"},
|
||||
Actions: true,
|
||||
ToolDefinitions: []ToolDefinition{{Name: "run", Description: "Run a command.", Kind: ToolKindAction,
|
||||
InputSchema: json.RawMessage(schema), Shadows: "bash", Execution: e}},
|
||||
}
|
||||
return m
|
||||
}
|
||||
host := func() HostCompatibility {
|
||||
h := compatible()
|
||||
h.SDK = 0
|
||||
h.API, h.APIMin = HostAPIVersion, HostAPIMin
|
||||
return h
|
||||
}
|
||||
commands := &ExecutionDescriptor{Class: ExecutionClassCommands, Title: "Runbox", Note: "It has python3 and jq."}
|
||||
|
||||
It("is valid for a tool that shadows bash when the manifest accepts API 2", func() {
|
||||
Expect(ValidateManifest(manifestWith(commands, VersionRange{Min: 1, Max: 2}), host())).To(Succeed())
|
||||
})
|
||||
It("needs the manifest to accept API 2", func() {
|
||||
Expect(ValidateManifest(manifestWith(commands, VersionRange{Min: 1, Max: 1}), host())).NotTo(Succeed())
|
||||
})
|
||||
It("is refused on a tool that does not shadow bash, with another class or with bad text", func() {
|
||||
m := manifestWith(commands, VersionRange{Min: 1, Max: 2})
|
||||
m.Capabilities.ToolDefinitions[0].Shadows = ""
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
for _, bad := range []*ExecutionDescriptor{
|
||||
{Class: "pages"},
|
||||
{Class: ExecutionClassCommands, Title: "<b>Runbox</b>"},
|
||||
{Class: ExecutionClassCommands, Note: string(make([]rune, MaxExecutionNoteChars+1))},
|
||||
{Class: ExecutionClassCommands, Skills: &ExecutionSkills{ProvidesRuntimeFor: []string{"games"}}},
|
||||
} {
|
||||
Expect(ValidateManifest(manifestWith(bad, VersionRange{Min: 1, Max: 2}), host())).NotTo(Succeed())
|
||||
}
|
||||
})
|
||||
|
||||
It("keeps a manifest of API 1 valid on a host that speaks API 2, and refuses one that needs API 3", func() {
|
||||
Expect(ValidateManifest(manifestWith(nil, VersionRange{Min: 1, Max: 1}), host())).To(Succeed())
|
||||
Expect(ValidateManifest(manifestWith(nil, VersionRange{Min: 3, Max: 3}), host())).NotTo(Succeed())
|
||||
})
|
||||
|
||||
Describe("IsolatedSkills", func() {
|
||||
skills := []SkillDefinition{{Name: "docx", Shadows: "docx"}, {Name: "runbox-tips"}}
|
||||
defs := func(e *ExecutionDescriptor) []ToolDefinition {
|
||||
return manifestWith(e, VersionRange{Min: 1, Max: 2}).Capabilities.ToolDefinitions
|
||||
}
|
||||
It("names the document skills of a provider that declares it supplies their runtime", func() {
|
||||
e := &ExecutionDescriptor{Class: ExecutionClassCommands, Skills: &ExecutionSkills{ProvidesRuntimeFor: []string{SkillKindDocuments}}}
|
||||
Expect(IsolatedSkills(defs(e), skills)).To(Equal([]string{"docx"}))
|
||||
})
|
||||
It("fails closed for a commands tool without a descriptor", func() {
|
||||
Expect(IsolatedSkills(defs(nil), skills)).To(Equal([]string{"docx"}))
|
||||
})
|
||||
It("names nothing for a descriptor that does not supply the runtime of documents", func() {
|
||||
Expect(IsolatedSkills(defs(commands), skills)).To(BeEmpty())
|
||||
})
|
||||
It("names nothing for an add-on without a commands tool", func() {
|
||||
Expect(IsolatedSkills(nil, skills)).To(BeEmpty())
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -58,9 +58,25 @@ func (r VersionRange) accepts(v uint64) bool {
|
||||
return r.Min > 0 && r.Max >= r.Min && v >= r.Min && v <= r.Max
|
||||
}
|
||||
|
||||
// HostAPIVersion is the manifest API version this host speaks. Version 2 adds
|
||||
// the execution descriptor of a tool. The host still accepts add-ons that
|
||||
// declare only version 1 (HostAPIMin), because every change since is optional.
|
||||
const (
|
||||
HostAPIVersion uint64 = 2
|
||||
HostAPIMin uint64 = 1
|
||||
)
|
||||
|
||||
// overlaps reports whether the range holds a version from lo to hi.
|
||||
func (r VersionRange) overlaps(lo, hi uint64) bool {
|
||||
return r.Min > 0 && r.Max >= r.Min && r.Min <= hi && r.Max >= lo
|
||||
}
|
||||
|
||||
type HostCompatibility struct {
|
||||
Host uint64 `json:"host"`
|
||||
API uint64 `json:"api"`
|
||||
Host uint64 `json:"host"`
|
||||
API uint64 `json:"api"`
|
||||
// APIMin is the oldest manifest API the host still accepts. Zero means API
|
||||
// only.
|
||||
APIMin uint64 `json:"apiMin,omitempty"`
|
||||
Protocol uint64 `json:"protocol"`
|
||||
SDK uint64 `json:"sdk"`
|
||||
DataFormat uint64 `json:"dataFormat"`
|
||||
@@ -347,7 +363,11 @@ func ValidateManifest(m Manifest, h HostCompatibility) error {
|
||||
if !namespace.MatchString(m.OS) || !namespace.MatchString(m.Arch) || m.OS != h.OS || m.Arch != h.Arch {
|
||||
return fmt.Errorf("incompatible platform")
|
||||
}
|
||||
if !m.Host.accepts(h.Host) || !m.API.accepts(h.API) || !m.Protocol.accepts(h.Protocol) || !m.DataFormat.accepts(h.DataFormat) {
|
||||
apiMin := h.APIMin
|
||||
if apiMin == 0 || apiMin > h.API {
|
||||
apiMin = h.API
|
||||
}
|
||||
if !m.Host.accepts(h.Host) || !m.API.overlaps(apiMin, h.API) || !m.Protocol.accepts(h.Protocol) || !m.DataFormat.accepts(h.DataFormat) {
|
||||
return fmt.Errorf("incompatible host, API, protocol or data format")
|
||||
}
|
||||
c := m.Capabilities
|
||||
@@ -355,6 +375,9 @@ func ValidateManifest(m Manifest, h HostCompatibility) error {
|
||||
return fmt.Errorf("invalid capabilities")
|
||||
}
|
||||
for _, d := range c.ToolDefinitions {
|
||||
if d.Execution != nil && m.API.Max < MinExecutionAPI {
|
||||
return fmt.Errorf("tool %s declares an execution descriptor, which needs api %d", d.Name, MinExecutionAPI)
|
||||
}
|
||||
if d.Kind == ToolKindAction && !c.Actions {
|
||||
return fmt.Errorf("tool %s is an action tool but the add-on does not declare actions", d.Name)
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ type ToolDefinition struct {
|
||||
// Only a name in ReplaceableTools may be shadowed. A tool without it keeps
|
||||
// its add-on name (addon__<id>__<tool>).
|
||||
Shadows string `json:"shadows,omitempty"`
|
||||
// Execution declares that this tool runs commands and what the host may
|
||||
// say about the provider. Only a tool that shadows bash may declare it, and
|
||||
// the manifest must then accept API 2 (see MinExecutionAPI).
|
||||
Execution *ExecutionDescriptor `json:"execution,omitempty"`
|
||||
}
|
||||
|
||||
// ReplaceableTool says what a tool that shadows a built-in chat tool must look
|
||||
@@ -222,6 +226,9 @@ func validateToolDefinitions(defs []ToolDefinition, tools []string) error {
|
||||
if d.UIOnly && !IsResourceless(d) {
|
||||
return fmt.Errorf("tool %s: a page-only tool must be a read tool without a resource", d.Name)
|
||||
}
|
||||
if err := d.Execution.validate(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if d.Shadows != "" {
|
||||
if err := ValidateShadow(d); err != nil {
|
||||
return err
|
||||
|
||||
@@ -32,7 +32,7 @@ import (
|
||||
// does not contain them is not installable and must not be published.
|
||||
func hostFor(platform string) contract.HostCompatibility {
|
||||
os_, arch, _ := strings.Cut(platform, "/")
|
||||
return contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: contract.HostSDKVersion, DataFormat: 1, OS: os_, Arch: arch}
|
||||
return contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, APIMin: contract.HostAPIMin, Protocol: 1, SDK: contract.HostSDKVersion, DataFormat: 1, OS: os_, Arch: arch}
|
||||
}
|
||||
|
||||
type fileEntry struct {
|
||||
|
||||
@@ -387,7 +387,17 @@
|
||||
"names every kind and status once",
|
||||
"tells a settled status from a pending one",
|
||||
"keeps the wire version and omits empty fields",
|
||||
"has the limits the owner decided"
|
||||
"has the limits the owner decided",
|
||||
"is valid for a tool that shadows bash when the manifest accepts API 2",
|
||||
"needs the manifest to accept API 2",
|
||||
"is refused on a tool that does not shadow bash, with another class or with bad text",
|
||||
"keeps a manifest of API 1 valid on a host that speaks API 2, and refuses one that needs API 3",
|
||||
"names the document skills of a provider that declares it supplies their runtime",
|
||||
"fails closed for a commands tool without a descriptor",
|
||||
"names nothing for a descriptor that does not supply the runtime of documents",
|
||||
"names nothing for an add-on without a commands tool",
|
||||
"reports network-policy enforcement only for an available isolated provider",
|
||||
"refuses a non-boolean enforcement claim"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+43
-127
@@ -3708,7 +3708,7 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "The saved agent's folder is missing or not chosen (agent_folder_missing), or its complete file access exceeds the folder limit (agent_folder_limit)",
|
||||
"description": "The saved agent's folder is missing or not chosen (agent_folder_missing)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.ErrorResponse"
|
||||
}
|
||||
@@ -4690,7 +4690,7 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/chats/agents/{id}/export": {
|
||||
"get": {
|
||||
"description": "Returns the definition as a JSON file (version, kind localai-agent, agent). It holds no ID, owner or time. An agent with a folder is version 2 and carries folder {path, mode}, the folder's home-relative path now (empty when the folder is missing, not chosen, or outside your home, even if readable); importing an empty path preserves the scope as unset with a warning; a server that knows only version 1 refuses it. An agent without a folder is version 1. POST /api/chats/agents/import reads it. This replaces the old /api/agents/{name}/export.",
|
||||
"description": "Returns the definition as a JSON file (version, kind localai-agent, agent). It holds no ID, owner or time. An agent with a folder is version 2 and carries folder {path, mode}, the folder's path in your files now (empty when the folder is missing or not chosen); a server that knows only version 1 refuses it. An agent without a folder is version 1. POST /api/chats/agents/import reads it. This replaces the old /api/agents/{name}/export.",
|
||||
"tags": [
|
||||
"chats"
|
||||
],
|
||||
@@ -9579,7 +9579,7 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "A turn is running for a command, or the saved agent's folder is missing (agent_folder_missing), or its complete file access exceeds the folder limit (agent_folder_limit)",
|
||||
"description": "A turn is running for a command, or the saved agent's folder is missing (agent_folder_missing)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.ErrorResponse"
|
||||
}
|
||||
@@ -14816,9 +14816,6 @@ const docTemplate = `{
|
||||
"canStop": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hostOperations": {
|
||||
"$ref": "#/definitions/hostops.RequestSummary"
|
||||
},
|
||||
"managementUrl": {
|
||||
"description": "ManagementURL opens the management view of the addon.",
|
||||
"type": "string",
|
||||
@@ -15018,32 +15015,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps.HomeCommandView": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"alias": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps.List": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15147,20 +15118,6 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"example": "/api/addons/notes/assets/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef/ui.js"
|
||||
},
|
||||
"homeCommands": {
|
||||
"description": "HomeCommands contains only resolved navigation metadata for this app.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apps.HomeCommandView"
|
||||
}
|
||||
},
|
||||
"modelActions": {
|
||||
"description": "ModelActions contains only declared destinations for this installed UI.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/contract.ModelAction"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -15523,20 +15480,43 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"contract.ModelAction": {
|
||||
"contract.ExecutionDescriptor": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capability": {
|
||||
"class": {
|
||||
"description": "Class is ExecutionClassCommands.",
|
||||
"type": "string"
|
||||
},
|
||||
"page": {
|
||||
"note": {
|
||||
"description": "Note is one plain sentence for the model when the provider reports\nisolated execution, such as the programs its environment has.",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"skills": {
|
||||
"description": "Skills says which skills run on this provider.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/contract.ExecutionSkills"
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"description": "Title names the provider in the text the host shows the person and the\nmodel, for example \"Sandbox\". It falls back to the add-on's title.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contract.ExecutionSkills": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"providesRuntimeFor": {
|
||||
"description": "ProvidesRuntimeFor lists skill kinds (SkillKindDocuments). The host\noffers the add-on's skills of those kinds only while the provider\nreports isolated execution.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contract.OutputDescription": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15671,11 +15651,11 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name is the skill's name and the name of its folder. A plain skill is\nnamed after the add-on: the add-on ID, or the ID and a dash and more\n(office-quickstart), so two add-ons never ship one plain name. A skill\nfor a reserved document slot has the slot's name (see Shadows).",
|
||||
"description": "Name is the skill's name and the name of its folder. A plain skill is\nnamed after the add-on: the add-on ID, or the ID and a dash and more\n(office-quickstart), so two add-ons never ship one plain name. A skill\nthat replaces a built-in one has the built-in's name (see Shadows).",
|
||||
"type": "string"
|
||||
},
|
||||
"shadows": {
|
||||
"description": "Shadows names the document-provider slot this skill supplies. It must equal Name\nand be one of ReplaceableSkills. It repeats the name on purpose: the\nreplacement is an explicit act, which the install warning and the check\nread from the manifest without opening the package. Empty: a plain\nadd-on skill.",
|
||||
"description": "Shadows names the built-in skill this skill replaces. It must equal Name\nand be one of ReplaceableSkills. It repeats the name on purpose: the\nreplacement is an explicit act, which the install warning and the check\nread from the manifest without opening the package. Empty: a plain\nadd-on skill.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -15687,6 +15667,14 @@ const docTemplate = `{
|
||||
"description": "Description is plain text for the model.",
|
||||
"type": "string"
|
||||
},
|
||||
"execution": {
|
||||
"description": "Execution declares that this tool runs commands and what the host may\nsay about the provider. Only a tool that shadows bash may declare it, and\nthe manifest must then accept API 2 (see MinExecutionAPI).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/contract.ExecutionDescriptor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputSchema": {
|
||||
"description": "InputSchema is the JSON schema of the arguments object. It is an\nobject schema with explicit properties, additionalProperties false at\nthe top level and the keywords the host allows only.",
|
||||
"type": "object"
|
||||
@@ -16982,50 +16970,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"hostops.RequestSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"granted": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"grantedCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"missingOptional": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"missingRequired": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"optional": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"optionalCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"required": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"requiredCount": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"library.Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -18248,9 +18192,6 @@ const docTemplate = `{
|
||||
"commands_reason": {
|
||||
"description": "CommandsReason is why commands cannot run, when Commands is unavailable.",
|
||||
"type": "string"
|
||||
},
|
||||
"execution_status": {
|
||||
"$ref": "#/definitions/schema.ChatsExecutionStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18586,7 +18527,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path is home-relative (\"/\" is the whole home), or /shared/\u003cname\u003e/...\nfor a followed readable shared folder. It is absent when missing,\nnot chosen, or readable outside these public locations; a readable\nfolder without a public path still has state ok.",
|
||||
"description": "Path is the folder's path in your files now (\"/\" is all of them). It\nis absent when the folder is missing or not chosen.",
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
@@ -18745,7 +18686,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"folder": {
|
||||
"description": "Folder chooses the one folder the agent's chats work in. A write\nwithout it keeps the folder the agent has; {\"path\": \"\"} clears it. In\nan export file it is the home-relative path and mode when the file was\nmade. A missing, unchosen or non-home folder has an empty export path;\nimport preserves its mode as unset and warns instead of clearing scope.",
|
||||
"description": "Folder chooses the one folder the agent's chats work in. A write\nwithout it keeps the folder the agent has; {\"path\": \"\"} clears it. In\nan export file it is the folder's path and mode when the file was made.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/schema.ChatsAgentFolderWrite"
|
||||
@@ -19643,7 +19584,7 @@ const docTemplate = `{
|
||||
}
|
||||
},
|
||||
"applies": {
|
||||
"description": "Applies is true when the add-on that runs the commands reports that it\nenforces the network policy. It is false when commands run on this\nmachine (applies_reason host), or no Sandbox serves bash or the add-on\ndoes not report enforcement (none).",
|
||||
"description": "Applies is false when commands run on this machine (applies_reason\nhost) or no Sandbox serves bash (none).",
|
||||
"type": "boolean"
|
||||
},
|
||||
"applies_reason": {
|
||||
@@ -19955,31 +19896,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ChatsExecutionStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"explanation": {
|
||||
"type": "string"
|
||||
},
|
||||
"generation": {
|
||||
"type": "string"
|
||||
},
|
||||
"incarnation": {
|
||||
"type": "string"
|
||||
},
|
||||
"isolation": {
|
||||
"type": "string"
|
||||
},
|
||||
"readiness": {
|
||||
"type": "string"
|
||||
},
|
||||
"unknown_reason": {
|
||||
"description": "UnknownReason says why the status is unknown: owner_unreachable,\nnot_reported, stale, invalid_status, not_selected, no_answer or\nreported_unknown. Absent when the status is known.",
|
||||
"type": "string",
|
||||
"example": "owner_unreachable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ChatsExport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+26756
File diff suppressed because it is too large.
Load diff
+43
-127
@@ -3705,7 +3705,7 @@
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "The saved agent's folder is missing or not chosen (agent_folder_missing), or its complete file access exceeds the folder limit (agent_folder_limit)",
|
||||
"description": "The saved agent's folder is missing or not chosen (agent_folder_missing)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.ErrorResponse"
|
||||
}
|
||||
@@ -4687,7 +4687,7 @@
|
||||
},
|
||||
"/api/chats/agents/{id}/export": {
|
||||
"get": {
|
||||
"description": "Returns the definition as a JSON file (version, kind localai-agent, agent). It holds no ID, owner or time. An agent with a folder is version 2 and carries folder {path, mode}, the folder's home-relative path now (empty when the folder is missing, not chosen, or outside your home, even if readable); importing an empty path preserves the scope as unset with a warning; a server that knows only version 1 refuses it. An agent without a folder is version 1. POST /api/chats/agents/import reads it. This replaces the old /api/agents/{name}/export.",
|
||||
"description": "Returns the definition as a JSON file (version, kind localai-agent, agent). It holds no ID, owner or time. An agent with a folder is version 2 and carries folder {path, mode}, the folder's path in your files now (empty when the folder is missing or not chosen); a server that knows only version 1 refuses it. An agent without a folder is version 1. POST /api/chats/agents/import reads it. This replaces the old /api/agents/{name}/export.",
|
||||
"tags": [
|
||||
"chats"
|
||||
],
|
||||
@@ -9576,7 +9576,7 @@
|
||||
}
|
||||
},
|
||||
"409": {
|
||||
"description": "A turn is running for a command, or the saved agent's folder is missing (agent_folder_missing), or its complete file access exceeds the folder limit (agent_folder_limit)",
|
||||
"description": "A turn is running for a command, or the saved agent's folder is missing (agent_folder_missing)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.ErrorResponse"
|
||||
}
|
||||
@@ -14813,9 +14813,6 @@
|
||||
"canStop": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hostOperations": {
|
||||
"$ref": "#/definitions/hostops.RequestSummary"
|
||||
},
|
||||
"managementUrl": {
|
||||
"description": "ManagementURL opens the management view of the addon.",
|
||||
"type": "string",
|
||||
@@ -15015,32 +15012,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps.HomeCommandView": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"alias": {
|
||||
"type": "string"
|
||||
},
|
||||
"icon": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"apps.List": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15144,20 +15115,6 @@
|
||||
"type": "string",
|
||||
"example": "/api/addons/notes/assets/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef/ui.js"
|
||||
},
|
||||
"homeCommands": {
|
||||
"description": "HomeCommands contains only resolved navigation metadata for this app.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/apps.HomeCommandView"
|
||||
}
|
||||
},
|
||||
"modelActions": {
|
||||
"description": "ModelActions contains only declared destinations for this installed UI.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/contract.ModelAction"
|
||||
}
|
||||
},
|
||||
"navigation": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -15520,20 +15477,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contract.ModelAction": {
|
||||
"contract.ExecutionDescriptor": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capability": {
|
||||
"class": {
|
||||
"description": "Class is ExecutionClassCommands.",
|
||||
"type": "string"
|
||||
},
|
||||
"page": {
|
||||
"note": {
|
||||
"description": "Note is one plain sentence for the model when the provider reports\nisolated execution, such as the programs its environment has.",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"skills": {
|
||||
"description": "Skills says which skills run on this provider.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/contract.ExecutionSkills"
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": {
|
||||
"description": "Title names the provider in the text the host shows the person and the\nmodel, for example \"Sandbox\". It falls back to the add-on's title.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contract.ExecutionSkills": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"providesRuntimeFor": {
|
||||
"description": "ProvidesRuntimeFor lists skill kinds (SkillKindDocuments). The host\noffers the add-on's skills of those kinds only while the provider\nreports isolated execution.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"contract.OutputDescription": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -15668,11 +15648,11 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "Name is the skill's name and the name of its folder. A plain skill is\nnamed after the add-on: the add-on ID, or the ID and a dash and more\n(office-quickstart), so two add-ons never ship one plain name. A skill\nfor a reserved document slot has the slot's name (see Shadows).",
|
||||
"description": "Name is the skill's name and the name of its folder. A plain skill is\nnamed after the add-on: the add-on ID, or the ID and a dash and more\n(office-quickstart), so two add-ons never ship one plain name. A skill\nthat replaces a built-in one has the built-in's name (see Shadows).",
|
||||
"type": "string"
|
||||
},
|
||||
"shadows": {
|
||||
"description": "Shadows names the document-provider slot this skill supplies. It must equal Name\nand be one of ReplaceableSkills. It repeats the name on purpose: the\nreplacement is an explicit act, which the install warning and the check\nread from the manifest without opening the package. Empty: a plain\nadd-on skill.",
|
||||
"description": "Shadows names the built-in skill this skill replaces. It must equal Name\nand be one of ReplaceableSkills. It repeats the name on purpose: the\nreplacement is an explicit act, which the install warning and the check\nread from the manifest without opening the package. Empty: a plain\nadd-on skill.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -15684,6 +15664,14 @@
|
||||
"description": "Description is plain text for the model.",
|
||||
"type": "string"
|
||||
},
|
||||
"execution": {
|
||||
"description": "Execution declares that this tool runs commands and what the host may\nsay about the provider. Only a tool that shadows bash may declare it, and\nthe manifest must then accept API 2 (see MinExecutionAPI).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/contract.ExecutionDescriptor"
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputSchema": {
|
||||
"description": "InputSchema is the JSON schema of the arguments object. It is an\nobject schema with explicit properties, additionalProperties false at\nthe top level and the keywords the host allows only.",
|
||||
"type": "object"
|
||||
@@ -16979,50 +16967,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"hostops.RequestSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"granted": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"grantedCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"missingOptional": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"missingRequired": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"optional": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"optionalCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"required": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"requiredCount": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"library.Info": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -18245,9 +18189,6 @@
|
||||
"commands_reason": {
|
||||
"description": "CommandsReason is why commands cannot run, when Commands is unavailable.",
|
||||
"type": "string"
|
||||
},
|
||||
"execution_status": {
|
||||
"$ref": "#/definitions/schema.ChatsExecutionStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -18583,7 +18524,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path is home-relative (\"/\" is the whole home), or /shared/\u003cname\u003e/...\nfor a followed readable shared folder. It is absent when missing,\nnot chosen, or readable outside these public locations; a readable\nfolder without a public path still has state ok.",
|
||||
"description": "Path is the folder's path in your files now (\"/\" is all of them). It\nis absent when the folder is missing or not chosen.",
|
||||
"type": "string"
|
||||
},
|
||||
"state": {
|
||||
@@ -18742,7 +18683,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"folder": {
|
||||
"description": "Folder chooses the one folder the agent's chats work in. A write\nwithout it keeps the folder the agent has; {\"path\": \"\"} clears it. In\nan export file it is the home-relative path and mode when the file was\nmade. A missing, unchosen or non-home folder has an empty export path;\nimport preserves its mode as unset and warns instead of clearing scope.",
|
||||
"description": "Folder chooses the one folder the agent's chats work in. A write\nwithout it keeps the folder the agent has; {\"path\": \"\"} clears it. In\nan export file it is the folder's path and mode when the file was made.",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/schema.ChatsAgentFolderWrite"
|
||||
@@ -19640,7 +19581,7 @@
|
||||
}
|
||||
},
|
||||
"applies": {
|
||||
"description": "Applies is true when the add-on that runs the commands reports that it\nenforces the network policy. It is false when commands run on this\nmachine (applies_reason host), or no Sandbox serves bash or the add-on\ndoes not report enforcement (none).",
|
||||
"description": "Applies is false when commands run on this machine (applies_reason\nhost) or no Sandbox serves bash (none).",
|
||||
"type": "boolean"
|
||||
},
|
||||
"applies_reason": {
|
||||
@@ -19952,31 +19893,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ChatsExecutionStatus": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"explanation": {
|
||||
"type": "string"
|
||||
},
|
||||
"generation": {
|
||||
"type": "string"
|
||||
},
|
||||
"incarnation": {
|
||||
"type": "string"
|
||||
},
|
||||
"isolation": {
|
||||
"type": "string"
|
||||
},
|
||||
"readiness": {
|
||||
"type": "string"
|
||||
},
|
||||
"unknown_reason": {
|
||||
"description": "UnknownReason says why the status is unknown: owner_unreachable,\nnot_reported, stale, invalid_status, not_selected, no_answer or\nreported_unknown. Absent when the status is known.",
|
||||
"type": "string",
|
||||
"example": "owner_unreachable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.ChatsExport": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+46
-108
@@ -166,8 +166,6 @@ definitions:
|
||||
type: boolean
|
||||
canStop:
|
||||
type: boolean
|
||||
hostOperations:
|
||||
$ref: '#/definitions/hostops.RequestSummary'
|
||||
managementUrl:
|
||||
description: ManagementURL opens the management view of the addon.
|
||||
example: /app/addons?manage=notes
|
||||
@@ -353,23 +351,6 @@ definitions:
|
||||
example: summarize
|
||||
type: string
|
||||
type: object
|
||||
apps.HomeCommandView:
|
||||
properties:
|
||||
alias:
|
||||
type: string
|
||||
icon:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
page:
|
||||
type: string
|
||||
type: object
|
||||
apps.List:
|
||||
properties:
|
||||
apps:
|
||||
@@ -460,18 +441,6 @@ definitions:
|
||||
package digest, so a new package is a new URL.
|
||||
example: /api/addons/notes/assets/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef/ui.js
|
||||
type: string
|
||||
homeCommands:
|
||||
description: HomeCommands contains only resolved navigation metadata for this
|
||||
app.
|
||||
items:
|
||||
$ref: '#/definitions/apps.HomeCommandView'
|
||||
type: array
|
||||
modelActions:
|
||||
description: ModelActions contains only declared destinations for this installed
|
||||
UI.
|
||||
items:
|
||||
$ref: '#/definitions/contract.ModelAction'
|
||||
type: array
|
||||
navigation:
|
||||
items:
|
||||
type: string
|
||||
@@ -743,15 +712,37 @@ definitions:
|
||||
thread:
|
||||
type: string
|
||||
type: object
|
||||
contract.ModelAction:
|
||||
contract.ExecutionDescriptor:
|
||||
properties:
|
||||
capability:
|
||||
class:
|
||||
description: Class is ExecutionClassCommands.
|
||||
type: string
|
||||
page:
|
||||
note:
|
||||
description: |-
|
||||
Note is one plain sentence for the model when the provider reports
|
||||
isolated execution, such as the programs its environment has.
|
||||
type: string
|
||||
path:
|
||||
skills:
|
||||
allOf:
|
||||
- $ref: '#/definitions/contract.ExecutionSkills'
|
||||
description: Skills says which skills run on this provider.
|
||||
title:
|
||||
description: |-
|
||||
Title names the provider in the text the host shows the person and the
|
||||
model, for example "Sandbox". It falls back to the add-on's title.
|
||||
type: string
|
||||
type: object
|
||||
contract.ExecutionSkills:
|
||||
properties:
|
||||
providesRuntimeFor:
|
||||
description: |-
|
||||
ProvidesRuntimeFor lists skill kinds (SkillKindDocuments). The host
|
||||
offers the add-on's skills of those kinds only while the provider
|
||||
reports isolated execution.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
contract.OutputDescription:
|
||||
properties:
|
||||
handle:
|
||||
@@ -849,11 +840,11 @@ definitions:
|
||||
Name is the skill's name and the name of its folder. A plain skill is
|
||||
named after the add-on: the add-on ID, or the ID and a dash and more
|
||||
(office-quickstart), so two add-ons never ship one plain name. A skill
|
||||
for a reserved document slot has the slot's name (see Shadows).
|
||||
that replaces a built-in one has the built-in's name (see Shadows).
|
||||
type: string
|
||||
shadows:
|
||||
description: |-
|
||||
Shadows names the document-provider slot this skill supplies. It must equal Name
|
||||
Shadows names the built-in skill this skill replaces. It must equal Name
|
||||
and be one of ReplaceableSkills. It repeats the name on purpose: the
|
||||
replacement is an explicit act, which the install warning and the check
|
||||
read from the manifest without opening the package. Empty: a plain
|
||||
@@ -865,6 +856,13 @@ definitions:
|
||||
description:
|
||||
description: Description is plain text for the model.
|
||||
type: string
|
||||
execution:
|
||||
allOf:
|
||||
- $ref: '#/definitions/contract.ExecutionDescriptor'
|
||||
description: |-
|
||||
Execution declares that this tool runs commands and what the host may
|
||||
say about the provider. Only a tool that shadows bash may declare it, and
|
||||
the manifest must then accept API 2 (see MinExecutionAPI).
|
||||
inputSchema:
|
||||
description: |-
|
||||
InputSchema is the JSON schema of the arguments object. It is an
|
||||
@@ -1870,35 +1868,6 @@ definitions:
|
||||
example: 4f1c
|
||||
type: string
|
||||
type: object
|
||||
hostops.RequestSummary:
|
||||
properties:
|
||||
granted:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
grantedCount:
|
||||
type: integer
|
||||
missingOptional:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
missingRequired:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
optional:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
optionalCount:
|
||||
type: integer
|
||||
required:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
requiredCount:
|
||||
type: integer
|
||||
type: object
|
||||
library.Info:
|
||||
properties:
|
||||
authReference:
|
||||
@@ -2784,8 +2753,6 @@ definitions:
|
||||
commands_reason:
|
||||
description: CommandsReason is why commands cannot run, when Commands is unavailable.
|
||||
type: string
|
||||
execution_status:
|
||||
$ref: '#/definitions/schema.ChatsExecutionStatus'
|
||||
type: object
|
||||
schema.ChatsAddonSwitches:
|
||||
properties:
|
||||
@@ -3033,10 +3000,8 @@ definitions:
|
||||
type: string
|
||||
path:
|
||||
description: |-
|
||||
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 is the folder's path in your files now ("/" is all of them). It
|
||||
is absent when the folder is missing or not chosen.
|
||||
type: string
|
||||
state:
|
||||
description: |-
|
||||
@@ -3163,9 +3128,7 @@ definitions:
|
||||
description: |-
|
||||
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.
|
||||
an export file it is the folder's path and mode when the file was made.
|
||||
instructions:
|
||||
type: string
|
||||
mcp_servers:
|
||||
@@ -3883,10 +3846,8 @@ definitions:
|
||||
type: array
|
||||
applies:
|
||||
description: |-
|
||||
Applies is true when the add-on that runs the commands reports that it
|
||||
enforces the network policy. It is false when commands run on this
|
||||
machine (applies_reason host), or no Sandbox serves bash or the add-on
|
||||
does not report enforcement (none).
|
||||
Applies is false when commands run on this machine (applies_reason
|
||||
host) or no Sandbox serves bash (none).
|
||||
type: boolean
|
||||
applies_reason:
|
||||
type: string
|
||||
@@ -4104,26 +4065,6 @@ definitions:
|
||||
- $ref: '#/definitions/schema.ChatsUsage'
|
||||
description: 'Usage is set on usage events: what one model request used.'
|
||||
type: object
|
||||
schema.ChatsExecutionStatus:
|
||||
properties:
|
||||
explanation:
|
||||
type: string
|
||||
generation:
|
||||
type: string
|
||||
incarnation:
|
||||
type: string
|
||||
isolation:
|
||||
type: string
|
||||
readiness:
|
||||
type: string
|
||||
unknown_reason:
|
||||
description: |-
|
||||
UnknownReason says why the status is unknown: owner_unreachable,
|
||||
not_reported, stale, invalid_status, not_selected, no_answer or
|
||||
reported_unknown. Absent when the status is known.
|
||||
example: owner_unreachable
|
||||
type: string
|
||||
type: object
|
||||
schema.ChatsExport:
|
||||
properties:
|
||||
created:
|
||||
@@ -12306,8 +12247,7 @@ paths:
|
||||
schema:
|
||||
$ref: '#/definitions/schema.ErrorResponse'
|
||||
"409":
|
||||
description: The saved agent's folder is missing or not chosen (agent_folder_missing),
|
||||
or its complete file access exceeds the folder limit (agent_folder_limit)
|
||||
description: The saved agent's folder is missing or not chosen (agent_folder_missing)
|
||||
schema:
|
||||
$ref: '#/definitions/schema.ErrorResponse'
|
||||
summary: Start a chat.
|
||||
@@ -13445,8 +13385,7 @@ paths:
|
||||
$ref: '#/definitions/schema.ErrorResponse'
|
||||
"409":
|
||||
description: A turn is running for a command, or the saved agent's folder
|
||||
is missing (agent_folder_missing), or its complete file access exceeds
|
||||
the folder limit (agent_folder_limit)
|
||||
is missing (agent_folder_missing)
|
||||
schema:
|
||||
$ref: '#/definitions/schema.ErrorResponse'
|
||||
summary: Send a message to a chat.
|
||||
@@ -14309,11 +14248,10 @@ paths:
|
||||
get:
|
||||
description: Returns the definition as a JSON file (version, kind localai-agent,
|
||||
agent). It holds no ID, owner or time. An agent with a folder is version 2
|
||||
and carries folder {path, mode}, the folder's home-relative path now (empty
|
||||
when the folder is missing, not chosen, or outside your home, even if readable);
|
||||
importing an empty path preserves the scope as unset with a warning; a server
|
||||
that knows only version 1 refuses it. An agent without a folder is version
|
||||
1. POST /api/chats/agents/import reads it. This replaces the old /api/agents/{name}/export.
|
||||
and carries folder {path, mode}, the folder's path in your files now (empty
|
||||
when the folder is missing or not chosen); a server that knows only version
|
||||
1 refuses it. An agent without a folder is version 1. POST /api/chats/agents/import
|
||||
reads it. This replaces the old /api/agents/{name}/export.
|
||||
parameters:
|
||||
- description: the agent's id
|
||||
in: path
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user