mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
refactor(addons): make manifest api 2 the only version
No release ever shipped add-ons, so there is no installed base on api 1.
The manifest must now declare api {"min": 2, "max": 2}; a manifest with
api 1, no api or a wider range is refused with a message that names the
range.
Every field that was gated on api 2 is now part of the contract, so the
Min*API constants and their gates are gone, and so are HostAPIMin and
the apiMin host field. Three older-manifest shims go with them: the
resource listing the host implied for one add-on, the legacyArgs hint
for arguments an older version of a tool took, and the tests that
checked the gates. All shipped manifests and the test fixtures declare
api 2.
Assisted-by: Claude:claude-sonnet-5-5 go test
This commit is contained in:
96 files changed
+161
-611
No files matched your search
@@ -6,7 +6,7 @@
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"host": { "min": 1, "max": 1 },
|
||||
"api": { "min": 1, "max": 1 },
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": { "min": 1, "max": 1 },
|
||||
"dataFormat": { "min": 1, "max": 1 },
|
||||
"capabilities": {
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
"min": 1,
|
||||
"max": 1
|
||||
},
|
||||
"api": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
},
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
|
||||
@@ -32,7 +32,7 @@ var _ = Describe("the manifest", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m, err := contract.ParseManifest(b)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
h := contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, APIMin: contract.HostAPIMin, Protocol: 1, SDK: 2, DataFormat: 1, OS: "linux", Arch: "amd64"}
|
||||
h := contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 2, DataFormat: 1, OS: "linux", Arch: "amd64"}
|
||||
Expect(contract.ValidateManifest(m, h)).To(Succeed())
|
||||
Expect(m.Capabilities.Tools).To(ConsistOf(tools.Declared()))
|
||||
for _, d := range m.Capabilities.ToolDefinitions {
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
"min": 1,
|
||||
"max": 1
|
||||
},
|
||||
"api": {
|
||||
"min": 2,
|
||||
"max": 2
|
||||
},
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
|
||||
@@ -35,7 +35,7 @@ var _ = Describe("typed declaration echo", func() {
|
||||
caps.HostOperations = &contract.HostOperations{Required: []string{"workspace.materialize"}, Optional: []string{"egress.check"}}
|
||||
}
|
||||
v := contract.VersionRange{Min: 1, Max: 1}
|
||||
manifest := contract.Manifest{SchemaVersion: 1, ID: "sample", Version: "1.0.0", Executable: "bin/sample", OS: "linux", Arch: "amd64", Host: v, API: v, Protocol: v, DataFormat: v, Capabilities: caps}
|
||||
manifest := contract.Manifest{SchemaVersion: 1, ID: "sample", Version: "1.0.0", Executable: "bin/sample", OS: "linux", Arch: "amd64", Host: v, API: contract.VersionRange{Min: 2, Max: 2}, Protocol: v, DataFormat: v, Capabilities: caps}
|
||||
b, e := json.Marshal(manifest)
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
parsed, e := contract.ParseManifest(b)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"host": { "min": 1, "max": 1 },
|
||||
"api": { "min": 2, "max": 2 },
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": { "min": 1, "max": 1 },
|
||||
"dataFormat": { "min": 1, "max": 1 },
|
||||
"capabilities": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"api": {
|
||||
"max": 2,
|
||||
"min": 1
|
||||
"min": 2
|
||||
},
|
||||
"arch": "amd64",
|
||||
"capabilities": {
|
||||
@@ -377,13 +377,7 @@
|
||||
"markdown"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"legacyArgs": [
|
||||
{
|
||||
"hint": "use the markdown string",
|
||||
"name": "blocks"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create-xlsx",
|
||||
@@ -414,13 +408,7 @@
|
||||
"csv"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"legacyArgs": [
|
||||
{
|
||||
"hint": "use the csv string",
|
||||
"name": "sheets"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "create-pptx",
|
||||
@@ -496,13 +484,7 @@
|
||||
"markdown"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"legacyArgs": [
|
||||
{
|
||||
"hint": "use the markdown string",
|
||||
"name": "blocks"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"tools": [
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
"min": 1,
|
||||
"max": 1
|
||||
},
|
||||
"api": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
},
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
|
||||
@@ -261,7 +261,6 @@ 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 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"api": {
|
||||
"max": 2,
|
||||
"min": 1
|
||||
"min": 2
|
||||
},
|
||||
"arch": "amd64",
|
||||
"capabilities": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"host": { "min": 1, "max": 1 },
|
||||
"api": { "min": 2, "max": 2 },
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": { "min": 1, "max": 1 },
|
||||
"dataFormat": { "min": 1, "max": 1 },
|
||||
"capabilities": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"host": { "min": 1, "max": 1 },
|
||||
"api": { "min": 1, "max": 1 },
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": { "min": 1, "max": 1 },
|
||||
"dataFormat": { "min": 1, "max": 1 },
|
||||
"capabilities": {
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
"min": 1,
|
||||
"max": 1
|
||||
},
|
||||
"api": {
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"host": { "min": 1, "max": 1 },
|
||||
"api": { "min": 1, "max": 1 },
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": { "min": 1, "max": 1 },
|
||||
"dataFormat": { "min": 1, "max": 1 },
|
||||
"capabilities": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"host": { "min": 1, "max": 1 },
|
||||
"api": { "min": 2, "max": 2 },
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": { "min": 1, "max": 1 },
|
||||
"dataFormat": { "min": 1, "max": 1 },
|
||||
"capabilities": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"host": { "min": 1, "max": 1 },
|
||||
"api": { "min": 1, "max": 1 },
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": { "min": 1, "max": 1 },
|
||||
"dataFormat": { "min": 1, "max": 1 },
|
||||
"capabilities": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"api": {
|
||||
"max": 1,
|
||||
"min": 1
|
||||
"max": 2,
|
||||
"min": 2
|
||||
},
|
||||
"arch": "amd64",
|
||||
"capabilities": {
|
||||
|
||||
@@ -32,7 +32,7 @@ var _ = Describe("add-on automatic upgrade setting", func() {
|
||||
requireAddonRuntime()
|
||||
c := config.AddonConfig{Enabled: true, InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "local",
|
||||
Root: filepath.Join(realTempDir(), "addons"), AdminCheck: func(context.Context, contract.Actor) error { return nil },
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
DeferCleanup(func() {
|
||||
_ = filepath.Walk(c.Root, func(p string, i os.FileInfo, e error) error {
|
||||
if e == nil && i.IsDir() {
|
||||
|
||||
@@ -107,7 +107,7 @@ var _ = Describe("split owner and frontends", func() {
|
||||
Enabled: true,
|
||||
InstanceID: transport.InstanceID, OwnerID: gateway.OwnerIdentity, OwnerAddress: ownerAddress,
|
||||
StorageID: "local", Root: filepath.Join(dataPath, "addons"), Transport: transport, OwnershipMode: auth.AddonModeLocalOwner,
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"},
|
||||
}
|
||||
if role == "frontend" {
|
||||
c.Root = ""
|
||||
|
||||
@@ -59,7 +59,7 @@ var _ = Describe("addon bootstrap", func() {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
c.Host = contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}
|
||||
c.Host = contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}
|
||||
manifest, e := os.ReadFile("../../tests/addons/fixtures/packages/manifest.json")
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
var b bytes.Buffer
|
||||
|
||||
@@ -983,7 +983,7 @@ func (a *Application) startAddons() error {
|
||||
}
|
||||
host := c.Host
|
||||
if host == (contract.HostCompatibility{}) {
|
||||
host = contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, APIMin: contract.HostAPIMin, Protocol: 1, SDK: contract.HostSDKVersion, DataFormat: 1, OS: runtime.GOOS, Arch: runtime.GOARCH}
|
||||
host = contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, 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}
|
||||
|
||||
@@ -132,7 +132,7 @@ var _ = Describe("execution binding owner resolver", func() {
|
||||
Expect(sources.Save(ctx, actor, catalog.Source{ID: "test", Name: "Test", URL: url, Enabled: true, AllowLoopbackHTTP: true})).To(Succeed())
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
DeferCleanup(cancel)
|
||||
a = &Application{applicationConfig: &config.ApplicationConfig{Context: runCtx, Addons: config.AddonConfig{Enabled: true, InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "local", Root: ownerRoot, AdminCheck: admin, OwnershipMode: auth.AddonModeLocalOwner, Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}}}
|
||||
a = &Application{applicationConfig: &config.ApplicationConfig{Context: runCtx, Addons: config.AddonConfig{Enabled: true, InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "local", Root: ownerRoot, AdminCheck: admin, OwnershipMode: auth.AddonModeLocalOwner, Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}}}
|
||||
|
||||
keys := filepath.Join(root, "keys")
|
||||
Expect(gateway.GenerateCarrierKeys(keys, []string{"127.0.0.1"})).To(Succeed())
|
||||
|
||||
@@ -64,14 +64,14 @@ func newAppsStack() *appsStack {
|
||||
archives := map[string][]byte{}
|
||||
for id, files := range map[string]map[string][]byte{
|
||||
"notes": {
|
||||
"manifest.json": []byte(`{"schemaVersion":1,"id":"notes","version":"1.0.0","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["add","list"],"ui":{"entry":"ui.js","css":["ui/style.css"],"sdk":{"min":1,"max":1},"pages":["home"],"navigation":["home"],"chatActions":["summarize"],"mainPage":"home","icon":"ui/icon.png","title":{"en":"Notes","it":"Appunti"},"description":"Keep notes."}}}`),
|
||||
"manifest.json": []byte(`{"schemaVersion":1,"id":"notes","version":"1.0.0","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["add","list"],"ui":{"entry":"ui.js","css":["ui/style.css"],"sdk":{"min":1,"max":1},"pages":["home"],"navigation":["home"],"chatActions":["summarize"],"mainPage":"home","icon":"ui/icon.png","title":{"en":"Notes","it":"Appunti"},"description":"Keep notes."}}}`),
|
||||
"ui.js": []byte("export default {};"),
|
||||
"ui/style.css": []byte(".notes{color:red}"),
|
||||
"ui/icon.png": s.icon,
|
||||
"secret.txt": []byte("owner only"),
|
||||
},
|
||||
"headless": {
|
||||
"manifest.json": []byte(`{"schemaVersion":1,"id":"headless","version":"1.0.0","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true,"tools":["query"]}}`),
|
||||
"manifest.json": []byte(`{"schemaVersion":1,"id":"headless","version":"1.0.0","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true,"tools":["query"]}}`),
|
||||
},
|
||||
} {
|
||||
var b bytes.Buffer
|
||||
@@ -105,7 +105,7 @@ func newAppsStack() *appsStack {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
c := catalog.Catalog{Sources: sources, TargetOS: "linux", TargetArch: "amd64"}
|
||||
cfg := managerpkg.OwnerConfig{InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "disk", Root: filepath.Join(root, "owner")}
|
||||
installer := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
installer := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
DeferCleanup(func() {
|
||||
_ = filepath.Walk(root, func(p string, info os.FileInfo, e error) error {
|
||||
if e == nil && info.IsDir() {
|
||||
|
||||
@@ -50,7 +50,7 @@ var _ = Describe("install refusal in the dispatch response", func() {
|
||||
actor := contract.Actor{UserID: u.ID, CredentialID: "key:" + key.ID, Mode: "named-user"}
|
||||
root := GinkgoT().TempDir()
|
||||
manifest := func(id string) string {
|
||||
return `{"schemaVersion":1,"id":"` + id + `","version":"1.0.0","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true,"ui":{"entry":"ui.js","sdk":{"min":2,"max":2},"pages":["home"],"mainPage":"home"}}}`
|
||||
return `{"schemaVersion":1,"id":"` + id + `","version":"1.0.0","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true,"ui":{"entry":"ui.js","sdk":{"min":2,"max":2},"pages":["home"],"mainPage":"home"}}}`
|
||||
}
|
||||
var releases []catalog.Release
|
||||
for _, id := range []string{"needs2", "corrupt"} {
|
||||
@@ -81,7 +81,7 @@ var _ = Describe("install refusal in the dispatch response", func() {
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
c := catalog.Catalog{Sources: sources, TargetOS: "linux", TargetArch: "amd64"}
|
||||
cfg := managerpkg.OwnerConfig{InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "disk", Root: filepath.Join(root, "owner")}
|
||||
installer := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
installer := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
DeferCleanup(func() {
|
||||
_ = filepath.Walk(root, func(p string, info os.FileInfo, e error) error {
|
||||
if e == nil && info.IsDir() {
|
||||
|
||||
@@ -49,7 +49,7 @@ type updatesStack struct {
|
||||
|
||||
func (s *updatesStack) publish(content string, requires catalog.Requirements) catalog.Release {
|
||||
GinkgoHelper()
|
||||
manifest := []byte(`{"schemaVersion":1,"id":"notes","version":"1.0.0-dev","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["add"]}}`)
|
||||
manifest := []byte(`{"schemaVersion":1,"id":"notes","version":"1.0.0-dev","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["add"]}}`)
|
||||
var b bytes.Buffer
|
||||
tw := tar.NewWriter(&b)
|
||||
for _, f := range []struct {
|
||||
@@ -114,7 +114,7 @@ func newUpdatesStack() *updatesStack {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
c := catalog.Catalog{Sources: s.sources, TargetOS: "linux", TargetArch: "amd64"}
|
||||
cfg := managerpkg.OwnerConfig{InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "disk", Root: filepath.Join(root, "owner")}
|
||||
installer := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
installer := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
DeferCleanup(func() {
|
||||
_ = filepath.Walk(root, func(p string, info os.FileInfo, e error) error {
|
||||
if e == nil && info.IsDir() {
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ export function installedAddon(over = {}) {
|
||||
id: 'notes', title: 'Notes', description: 'Keep notes.', icon: '/api/addons/apps/notes/icon?generation=3',
|
||||
version: '1.2.0', generation: 3, enabled: true, state: 'running', sourceId: 'main', selection: '1.2.0',
|
||||
verification: 'sha256+sigstore', digest: 'sha256:0123456789abcdef0123456789abcdef', platform: { os: 'linux', arch: 'amd64' },
|
||||
compatibility: { host: { min: 1, max: 2 }, api: { min: 1, max: 1 }, protocol: { min: 1, max: 1 }, dataFormat: { min: 1, max: 2 } },
|
||||
compatibility: { host: { min: 1, max: 2 }, api: { min: 2, max: 2 }, protocol: { min: 1, max: 1 }, dataFormat: { min: 1, max: 2 } },
|
||||
tools: ['add', 'list'], toolCount: 2, hasUI: true, mainPage: '/app/addons/notes/home', launchMode: 'always',
|
||||
generations: 3, restarts: 0, storage: { bytes: 12288, truncated: false }, ...over,
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ func newAuthModesServer(ownership string, opts ...config.AppOption) (*applicatio
|
||||
InstanceID: "local", OwnerID: "local", OwnerAddress: transport.OwnerAddress,
|
||||
StorageID: "local", Root: root, ListenAddress: address, Transport: transport,
|
||||
OwnershipMode: ownership,
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
}),
|
||||
}, opts...)
|
||||
app, err := application.New(all...)
|
||||
|
||||
@@ -94,7 +94,7 @@ var _ = Describe("Routes behind the addons gate", func() {
|
||||
InstanceID: "local", OwnerID: "local", OwnerAddress: transport.OwnerAddress,
|
||||
StorageID: "local", Root: root, ListenAddress: address, Transport: transport,
|
||||
OwnershipMode: "local-owner",
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"},
|
||||
}))
|
||||
e := echo.New()
|
||||
gate := httpMiddleware.RequireAddonsFeature(app)
|
||||
|
||||
@@ -73,7 +73,7 @@ var _ = Describe("GET /api/features and the addons gate", func() {
|
||||
InstanceID: "local", OwnerID: "local", OwnerAddress: transport.OwnerAddress,
|
||||
StorageID: "local", Root: root, ListenAddress: address, Transport: transport,
|
||||
OwnershipMode: "local-owner",
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"},
|
||||
}))
|
||||
Expect(got).To(HaveKeyWithValue("addons", true))
|
||||
Expect(got).To(HaveKeyWithValue("fine_tuning", true))
|
||||
|
||||
@@ -51,7 +51,7 @@ var _ = Describe("library routes through a split owner", func() {
|
||||
InstanceID: transport.InstanceID, OwnerID: gateway.OwnerIdentity, OwnerAddress: address,
|
||||
OfficialLibrary: true, OfficialBackendLibrary: true,
|
||||
StorageID: "local", Transport: transport, OwnershipMode: auth.AddonModeLocalOwner,
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
}
|
||||
if role == "frontend" {
|
||||
cfg.Frontend = true
|
||||
|
||||
@@ -419,8 +419,7 @@ func contributions(u *contract.UI) ([]ChatActionView, []SlotView, []contract.Ren
|
||||
return actions, slots, slices.Clone(u.Renderers)
|
||||
}
|
||||
|
||||
// resourceTools copies the resource mapping of the manifest and fills the
|
||||
// listing declaration an older manifest implied (see legacyResourceList).
|
||||
// resourceTools copies the resource mapping of the manifest.
|
||||
func resourceTools(m contract.Manifest) *contract.ResourceTools {
|
||||
if m.Capabilities.Resources == nil {
|
||||
return nil
|
||||
@@ -429,8 +428,6 @@ func resourceTools(m contract.Manifest) *contract.ResourceTools {
|
||||
if r.List != nil {
|
||||
l := *r.List
|
||||
r.List = &l
|
||||
} else {
|
||||
r.List = legacyResourceList(m)
|
||||
}
|
||||
return &r
|
||||
}
|
||||
@@ -19,7 +19,7 @@ const secretDir = "/srv/owner-only/packages/package-abc"
|
||||
|
||||
func snapshot(id string, enabled bool, state string, ui *contract.UI, tools ...string) Snapshot {
|
||||
v := contract.VersionRange{Min: 1, Max: 1}
|
||||
m := contract.Manifest{SchemaVersion: 1, ID: id, Version: "1.2.3", Executable: "bin/x", OS: "linux", Arch: "amd64", Host: v, API: v, Protocol: v, DataFormat: v, Capabilities: contract.Capabilities{Service: true, Tools: tools, UI: ui}}
|
||||
m := contract.Manifest{SchemaVersion: 1, ID: id, Version: "1.2.3", Executable: "bin/x", OS: "linux", Arch: "amd64", Host: v, API: contract.VersionRange{Min: 2, Max: 2}, Protocol: v, DataFormat: v, Capabilities: contract.Capabilities{Service: true, Tools: tools, UI: ui}}
|
||||
r := packages.PackageReceipt{Digest: "sha256:aaaa", SourceID: "main", SourceURL: "https://catalog.example/private", Reference: "oci://registry.example/x", Manifest: m, Directory: secretDir}
|
||||
if ui != nil && ui.Icon != "" {
|
||||
r.Icon = &packages.PackageIcon{Path: ui.Icon, Digest: "sha256:bbbb", Size: 10, MediaType: "image/png"}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package apps
|
||||
|
||||
import (
|
||||
"slices"
|
||||
|
||||
"github.com/mudler/LocalAI/pkg/addons/contract"
|
||||
)
|
||||
|
||||
// This file is a compatibility shim and the only place in core that names an
|
||||
// add-on for resource listing.
|
||||
//
|
||||
// Before the manifest could declare a listing (capabilities.resources.list),
|
||||
// the host offered one add-on a listing tool that the manifest did not
|
||||
// mention. That add-on now declares it. An installed copy that still has the
|
||||
// older manifest (API 1) keeps the tool through this shim until it updates.
|
||||
//
|
||||
// Removal: delete this file, resources_test.go's "legacy resource listing"
|
||||
// block and the call in build once contract.HostAPIMin reaches
|
||||
// contract.MinResourceListAPI. The last spec of that block fails then.
|
||||
|
||||
// legacyListingAddon is the add-on whose older manifests the shim covers.
|
||||
const legacyListingAddon = "tables"
|
||||
|
||||
// legacyResourceList returns the declaration an older manifest of the legacy
|
||||
// add-on would have made, or nil when the shim does not apply.
|
||||
func legacyResourceList(m contract.Manifest) *contract.ResourceList {
|
||||
r := m.Capabilities.Resources
|
||||
if m.ID != legacyListingAddon || r == nil || r.List != nil || m.API.Max >= contract.MinResourceListAPI {
|
||||
return nil
|
||||
}
|
||||
const tool = "databases-list"
|
||||
if slices.Contains(m.Capabilities.Tools, tool) {
|
||||
return nil
|
||||
}
|
||||
return &contract.ResourceList{Tool: tool, Items: "databases"}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
package apps
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAI/pkg/addons/contract"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -41,40 +39,3 @@ var _ = Describe("resource listing in the apps list", func() {
|
||||
Expect(a.Resources.List).To(BeNil())
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("legacy resource listing", func() {
|
||||
old := contract.VersionRange{Min: 1, Max: 1}
|
||||
mapping := func() *contract.ResourceTools {
|
||||
return &contract.ResourceTools{Noun: "database", Inspect: "databases-inspect"}
|
||||
}
|
||||
It("keeps the listing of a manifest written before the declaration existed", func() {
|
||||
a := Build([]Snapshot{resourceSnapshot(legacyListingAddon, old, mapping(), "databases-inspect")}, false)[0]
|
||||
Expect(a.Resources.List).To(Equal(&contract.ResourceList{Tool: "databases-list", Items: "databases"}))
|
||||
})
|
||||
It("does not touch an old manifest of another add-on, or one that ships a tool of that name", func() {
|
||||
Expect(Build([]Snapshot{resourceSnapshot("boards", old, mapping(), "databases-inspect")}, false)[0].Resources.List).To(BeNil())
|
||||
s := resourceSnapshot(legacyListingAddon, old, mapping(), "databases-inspect", "databases-list")
|
||||
Expect(Build([]Snapshot{s}, false)[0].Resources.List).To(BeNil())
|
||||
})
|
||||
It("never overrides a declaration", func() {
|
||||
decl := &contract.ResourceTools{Noun: "database", List: &contract.ResourceList{Tool: "dbs-list"}}
|
||||
a := Build([]Snapshot{resourceSnapshot(legacyListingAddon, contract.VersionRange{Min: 1, Max: 2}, decl, "databases-inspect")}, false)[0]
|
||||
Expect(a.Resources.List.Tool).To(Equal("dbs-list"))
|
||||
})
|
||||
It("is not needed by the shipped add-on, which declares the listing itself", func() {
|
||||
raw, err := os.ReadFile("../../../../addons/" + legacyListingAddon + "/manifest.json")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m, err := contract.ParseManifest(raw)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(m.Capabilities.Resources.List).NotTo(BeNil())
|
||||
Expect(legacyResourceList(m)).To(BeNil())
|
||||
})
|
||||
It("goes away when the host stops accepting manifests of API 1", func() {
|
||||
_, err := os.Stat("legacy_resource_list.go")
|
||||
if contract.HostAPIMin >= contract.MinResourceListAPI {
|
||||
Expect(os.IsNotExist(err)).To(BeTrue(), "every add-on now declares the listing: delete legacy_resource_list.go and its test")
|
||||
} else {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -113,7 +113,7 @@ var _ = Describe("apps and stale lifecycle generations", func() {
|
||||
v := contract.VersionRange{Min: 1, Max: 1}
|
||||
ui := &contract.UI{Entry: "ui.js", SDK: v, Pages: []string{"home"}, MainPage: "home", Icon: "icon.png"}
|
||||
receipt := func(id string, withIcon bool) packages.PackageReceipt {
|
||||
r := packages.PackageReceipt{Directory: dir, Manifest: contract.Manifest{SchemaVersion: 1, ID: id, Version: "1.0.0", Executable: "bin/x", OS: "linux", Arch: "amd64", Host: v, API: v, Protocol: v, DataFormat: v, Capabilities: contract.Capabilities{Service: true, UI: ui}}}
|
||||
r := packages.PackageReceipt{Directory: dir, Manifest: contract.Manifest{SchemaVersion: 1, ID: id, Version: "1.0.0", Executable: "bin/x", OS: "linux", Arch: "amd64", Host: v, API: contract.VersionRange{Min: 2, Max: 2}, Protocol: v, DataFormat: v, Capabilities: contract.Capabilities{Service: true, UI: ui}}}
|
||||
if withIcon {
|
||||
r.Icon = &packages.PackageIcon{Path: "icon.png", Digest: fmt.Sprintf("sha256:%x", sum), Size: int64(len(icon)), MediaType: "image/png"}
|
||||
}
|
||||
@@ -186,7 +186,7 @@ var _ = Describe("immutable app assets", Label("assets"), func() {
|
||||
v := contract.VersionRange{Min: 1, Max: 1}
|
||||
hex := func(c string) string { return strings.Repeat(c, 64) }
|
||||
receipt := func(id, digest string, ui *contract.UI) packages.PackageReceipt {
|
||||
return packages.PackageReceipt{Directory: dir, Digest: "sha256:" + digest, Manifest: contract.Manifest{SchemaVersion: 1, ID: id, Version: "1.0.0", Executable: "bin/x", OS: "linux", Arch: "amd64", Host: v, API: v, Protocol: v, DataFormat: v, Capabilities: contract.Capabilities{Service: true, UI: ui}}}
|
||||
return packages.PackageReceipt{Directory: dir, Digest: "sha256:" + digest, Manifest: contract.Manifest{SchemaVersion: 1, ID: id, Version: "1.0.0", Executable: "bin/x", OS: "linux", Arch: "amd64", Host: v, API: contract.VersionRange{Min: 2, Max: 2}, Protocol: v, DataFormat: v, Capabilities: contract.Capabilities{Service: true, UI: ui}}}
|
||||
}
|
||||
ui := &contract.UI{Entry: "ui.js", SDK: v, Pages: []string{"home"}}
|
||||
old := receipt("on", hex("1"), ui)
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
var _ = Describe("Host requirements carrier", func() {
|
||||
It("preserves bounded owner admission refusals over mTLS without frontend grants", func() {
|
||||
actor := contract.Actor{UserID: "admin", CredentialID: "key:admin", Mode: "named-user"}
|
||||
raw := `{"schemaVersion":1,"id":"sample","version":"1.2.3","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true,"hostOperations":{"required":["workspace.materialize"]}}}`
|
||||
raw := `{"schemaVersion":1,"id":"sample","version":"1.2.3","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true,"hostOperations":{"required":["workspace.materialize"]}}}`
|
||||
m := ownerManagerPackage(actor, false, raw, nil, 1, map[string][]string{})
|
||||
owner := &Gateway{Manager: m, Revalidate: func(context.Context, contract.Actor) error { return nil }}
|
||||
_, cs := carrierFixture(func(c *Carrier) http.Handler {
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
const uiManifest = `{"schemaVersion":1,"id":"sample","version":"1.2.3","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true,"ui":{"entry":"ui.js","css":["ui.css"],"sdk":{"min":1,"max":1}}}}`
|
||||
const uiManifest = `{"schemaVersion":1,"id":"sample","version":"1.2.3","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true,"ui":{"entry":"ui.js","css":["ui.css"],"sdk":{"min":1,"max":1}}}}`
|
||||
|
||||
// ownerManager builds a real owner journal. With a UI package it also installs
|
||||
// the addon "sample" through the real catalog and installer, operation "install".
|
||||
@@ -72,7 +72,7 @@ func ownerManagerPackage(actor contract.Actor, install bool, manifest string, fi
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
c := catalog.Catalog{Sources: sources, TargetOS: "linux", TargetArch: "amd64"}
|
||||
cfg := addons.OwnerConfig{InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "disk", Root: filepath.Join(root, "owner")}
|
||||
i := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: sdk, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
i := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: sdk, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
DeferCleanup(func() {
|
||||
_ = filepath.Walk(root, func(p string, info os.FileInfo, e error) error {
|
||||
if e == nil && info.IsDir() {
|
||||
|
||||
@@ -110,7 +110,7 @@ func fixtureWithFiles(manifest []byte, digest string, files map[string][]byte) (
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
c := catalog.Catalog{Sources: sources, TargetOS: "linux", TargetArch: "amd64"}
|
||||
cfg := OwnerConfig{InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "disk-one", Root: filepath.Join(root, "owner")}
|
||||
i := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
i := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
DeferCleanup(func() {
|
||||
_ = filepath.Walk(root, func(p string, info os.FileInfo, e error) error {
|
||||
if e == nil && info.IsDir() {
|
||||
|
||||
@@ -101,7 +101,7 @@ func fixture(data []byte, edits ...func(*catalog.Release, *catalog.Source)) (*In
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return &Installer{Root: root, Catalog: c, Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: r.OS, Arch: r.Arch}}, resolved
|
||||
return &Installer{Root: root, Catalog: c, Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: r.OS, Arch: r.Arch}}, resolved
|
||||
}
|
||||
|
||||
var _ = Describe("immutable packages", func() {
|
||||
@@ -163,7 +163,7 @@ var _ = Describe("immutable packages", func() {
|
||||
Expect(string(b)).To(Equal("old"))
|
||||
})
|
||||
It("rejects platform data API SDK and missing asset mismatches", func() {
|
||||
for _, change := range []func(*contract.Manifest){func(m *contract.Manifest) { m.OS = "darwin" }, func(m *contract.Manifest) { m.API.Min = 2; m.API.Max = 2 }, func(m *contract.Manifest) { m.DataFormat.Min = 2; m.DataFormat.Max = 2 }, func(m *contract.Manifest) {
|
||||
for _, change := range []func(*contract.Manifest){func(m *contract.Manifest) { m.OS = "darwin" }, func(m *contract.Manifest) { m.API.Min = 3; m.API.Max = 3 }, func(m *contract.Manifest) { m.DataFormat.Min = 2; m.DataFormat.Max = 2 }, func(m *contract.Manifest) {
|
||||
m.Capabilities.UI = &contract.UI{Entry: "ui.js", SDK: contract.VersionRange{Min: 2, Max: 2}}
|
||||
}, func(m *contract.Manifest) { m.Executable = "missing" }} {
|
||||
var manifest contract.Manifest
|
||||
|
||||
@@ -1074,7 +1074,7 @@ var _ = Describe("real manager lifecycle", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
cat := catalog.Catalog{Sources: sources, TargetOS: "linux", TargetArch: "amd64"}
|
||||
cfg := addons.OwnerConfig{InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "disk", Root: filepath.Join(root, "owner")}
|
||||
inst := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: cat, Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
inst := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: cat, Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
m, err := addons.NewManager(ctx, cfg, cat, inst, check)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
DeferCleanup(func() { _ = m.Close() })
|
||||
|
||||
@@ -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: contract.HostAPIVersion, APIMin: contract.HostAPIMin, Protocol: 1, SDK: contract.HostSDKVersion, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH}
|
||||
host := contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, 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
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func fixture(mode string, capabilities contract.Capabilities) packages.PackageRe
|
||||
Expect(os.WriteFile(filepath.Join(dir, "fixture"), b, 0500)).To(Succeed())
|
||||
Expect(os.WriteFile(filepath.Join(dir, "fixture.json"), []byte(`{"mode":"`+mode+`"}`), 0600)).To(Succeed())
|
||||
v := contract.VersionRange{Min: 1, Max: 1}
|
||||
m := contract.Manifest{SchemaVersion: 1, ID: "sample", Version: "1.0.0", Executable: "fixture", OS: goruntime.GOOS, Arch: goruntime.GOARCH, Host: v, API: v, Protocol: v, DataFormat: v, Capabilities: capabilities}
|
||||
m := contract.Manifest{SchemaVersion: 1, ID: "sample", Version: "1.0.0", Executable: "fixture", OS: goruntime.GOOS, Arch: goruntime.GOARCH, Host: v, API: contract.VersionRange{Min: 2, Max: 2}, Protocol: v, DataFormat: v, Capabilities: capabilities}
|
||||
p := packages.PackageReceipt{Manifest: m, Directory: dir, Digest: "sha256:fixture"}
|
||||
b, e = json.Marshal(p)
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
|
||||
@@ -722,7 +722,6 @@ func (t *ToolAdapter) execute(ctx context.Context, actor contract.Actor, convers
|
||||
var app apps.App
|
||||
var def ToolDefinition
|
||||
var found bool
|
||||
var declared contract.ToolDefinition
|
||||
for _, a := range list {
|
||||
if a.ID != addonID {
|
||||
continue
|
||||
@@ -738,7 +737,7 @@ func (t *ToolAdapter) execute(ctx context.Context, actor contract.Actor, convers
|
||||
continue // not offered to this consumer, so not there
|
||||
}
|
||||
if x, ok := definition(a, d); ok {
|
||||
app, def, found, declared = a, x, true, d
|
||||
app, def, found = a, x, true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -747,11 +746,6 @@ func (t *ToolAdapter) execute(ctx context.Context, actor contract.Actor, convers
|
||||
}
|
||||
resource, body, err := toolargs.Bind(def.Tool, def.Parameters, arguments, conversationID != "")
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrInvalidArguments) {
|
||||
if message := contract.LegacyMessage(declared, arguments); message != "" {
|
||||
return ToolResult{Kind: ResultError, Tool: name, Code: CodeInvalidRequest, Message: message}, nil
|
||||
}
|
||||
}
|
||||
return ToolResult{}, err
|
||||
}
|
||||
if resource == "" && conversationID != "" && requiresResource(def) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
func host() contract.HostCompatibility {
|
||||
return contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: runtime.GOOS, Arch: runtime.GOARCH}
|
||||
return contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: runtime.GOOS, Arch: runtime.GOARCH}
|
||||
}
|
||||
|
||||
// manifestWith validates the Tables manifest after a change to its tool
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package tools_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/mudler/LocalAI/core/services/addons/apps"
|
||||
"github.com/mudler/LocalAI/core/services/addons/tools"
|
||||
"github.com/mudler/LocalAI/pkg/addons/contract"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Legacy arguments declared by a tool", func() {
|
||||
It("names the replacement string for the legacy arguments the shipped Office manifest declares, without sending them", func() {
|
||||
raw, err := os.ReadFile("../../../../addons/office/manifest.json")
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m, err := contract.ParseManifest(raw)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
f := &actFake{fake: &fake{list: apps.List{Apps: []apps.App{{ID: "office", Enabled: true, State: "running", Generation: 1, Tools: m.Capabilities.Tools, ToolDefinitions: m.Capabilities.ToolDefinitions}}}}}
|
||||
a := tools.NewChatToolAdapter(f)
|
||||
for _, tc := range []struct{ tool, legacy, field string }{{"create-docx", "blocks", "markdown"}, {"create-pdf", "blocks", "markdown"}, {"create-xlsx", "sheets", "csv"}} {
|
||||
for _, mixed := range []bool{false, true} {
|
||||
args := map[string]any{"path": "/new", tc.legacy: []any{map[string]any{"text": "PRIVATE"}}}
|
||||
if mixed {
|
||||
args[tc.field] = "new"
|
||||
}
|
||||
b, err := json.Marshal(args)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
res, err := a.ExecuteChat(context.Background(), member, tools.NewChatCall("bob", "conv1"), tools.ToolName("office", tc.tool), b)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(res.Kind).To(Equal(tools.ResultError))
|
||||
Expect(res.Code).To(Equal(tools.CodeInvalidRequest))
|
||||
Expect(res.Message).To(ContainSubstring(tc.legacy))
|
||||
Expect(res.Message).To(ContainSubstring(tc.field))
|
||||
Expect(res.JSON()).NotTo(ContainSubstring("PRIVATE"))
|
||||
}
|
||||
}
|
||||
Expect(f.acts).To(BeEmpty())
|
||||
Expect(f.calls()).To(BeZero())
|
||||
})
|
||||
|
||||
It("works for any add-on that declares legacyArgs, and for none that does not", func() {
|
||||
schema := json.RawMessage(`{"type":"object","properties":{"path":{"type":"string"},"text":{"type":"string"}},"required":["path","text"],"additionalProperties":false}`)
|
||||
def := func(legacy ...contract.LegacyArgument) contract.ToolDefinition {
|
||||
return contract.ToolDefinition{Name: "make-note", Description: "Make a note.", Kind: contract.ToolKindAction, InputSchema: schema, LegacyArgs: legacy}
|
||||
}
|
||||
call := func(d contract.ToolDefinition) tools.ToolResult {
|
||||
f := &actFake{fake: &fake{list: apps.List{Apps: []apps.App{{ID: "notes", Enabled: true, State: "running", Generation: 1, Tools: []string{"make-note"}, ToolDefinitions: []contract.ToolDefinition{d}}}}}}
|
||||
res, err := tools.NewChatToolAdapter(f).ExecuteChat(context.Background(), member, tools.NewChatCall("bob", "conv1"), tools.ToolName("notes", "make-note"), json.RawMessage(`{"path":"/n","body":"PRIVATE"}`))
|
||||
if err != nil {
|
||||
return tools.ToolResult{}
|
||||
}
|
||||
return res
|
||||
}
|
||||
res := call(def(contract.LegacyArgument{Name: "body", Hint: "use the text string"}))
|
||||
Expect(res.Code).To(Equal(tools.CodeInvalidRequest))
|
||||
Expect(res.Message).To(Equal("body is no longer supported; use the text string"))
|
||||
Expect(call(def()).Message).To(BeEmpty(), "a tool that declares nothing keeps the plain refusal")
|
||||
})
|
||||
})
|
||||
@@ -13,19 +13,17 @@ import (
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// Core offers abilities to add-ons by declaration. It names no add-on: the
|
||||
// only exception is the marked compatibility file listed below.
|
||||
// Core offers abilities to add-ons by declaration. It names no add-on.
|
||||
var _ = Describe("add-on ids in core code", func() {
|
||||
dirs := []string{".", "../apps", "../gateway", "../resources", "../../../../pkg/addons/contract"}
|
||||
allowed := map[string]bool{"legacy_resource_list.go": true}
|
||||
|
||||
It("keeps the id of the Tables add-on out of string literals, outside tests and the marked shim", func() {
|
||||
It("keeps the id of the Tables add-on out of string literals, outside tests", func() {
|
||||
var hits []string
|
||||
for _, dir := range dirs {
|
||||
files, err := filepath.Glob(filepath.Join(dir, "*.go"))
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
for _, path := range files {
|
||||
if strings.HasSuffix(path, "_test.go") || allowed[filepath.Base(path)] {
|
||||
if strings.HasSuffix(path, "_test.go") {
|
||||
continue
|
||||
}
|
||||
f, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
|
||||
|
||||
@@ -18,7 +18,7 @@ var _ = Describe("declared tools of the installed generation", Label("tools"), f
|
||||
m := open(cfg, c, i)
|
||||
v := contract.VersionRange{Min: 1, Max: 1}
|
||||
receipt := func(id string, tools ...string) packages.PackageReceipt {
|
||||
return packages.PackageReceipt{Manifest: contract.Manifest{SchemaVersion: 1, ID: id, Version: "1.0.0", Executable: "bin/x", OS: "linux", Arch: "amd64", Host: v, API: v, Protocol: v, DataFormat: v, Capabilities: contract.Capabilities{Service: true, Tools: tools}}}
|
||||
return packages.PackageReceipt{Manifest: contract.Manifest{SchemaVersion: 1, ID: id, Version: "1.0.0", Executable: "bin/x", OS: "linux", Arch: "amd64", Host: v, API: contract.VersionRange{Min: 2, Max: 2}, Protocol: v, DataFormat: v, Capabilities: contract.Capabilities{Service: true, Tools: tools}}}
|
||||
}
|
||||
m.journal.Addons["notes"] = Status{AddonID: "notes", Installed: true, DesiredEnabled: true, Generation: 2, Generations: []Generation{
|
||||
{Number: 1, Receipt: receipt("notes", "old-tool")}, {Number: 2, Receipt: receipt("notes", "summarize")}}}
|
||||
@@ -123,7 +123,7 @@ var _ = Describe("declared capabilities of the installed generation", Label("too
|
||||
m.SetHostOps(map[string][]string{"notes": {"workspace."}})
|
||||
v := contract.VersionRange{Min: 1, Max: 1}
|
||||
m.journal.Addons["notes"] = Status{AddonID: "notes", Installed: true, DesiredEnabled: true, Generation: 1, Generations: []Generation{{Number: 1, Receipt: packages.PackageReceipt{
|
||||
Manifest: contract.Manifest{SchemaVersion: 1, ID: "notes", Version: "1.0.0", Executable: "bin/x", OS: "linux", Arch: "amd64", Host: v, API: v, Protocol: v, DataFormat: v, Capabilities: caps}}}}}
|
||||
Manifest: contract.Manifest{SchemaVersion: 1, ID: "notes", Version: "1.0.0", Executable: "bin/x", OS: "linux", Arch: "amd64", Host: v, API: contract.VersionRange{Min: 2, Max: 2}, Protocol: v, DataFormat: v, Capabilities: caps}}}}}
|
||||
got, e := m.DeclaredCapabilities(ctx, "notes", 1)
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
typ := reflect.TypeOf(caps)
|
||||
|
||||
@@ -143,7 +143,7 @@ func installed(version, selection, channel, digest string) updates.Installation
|
||||
}}
|
||||
}
|
||||
|
||||
var host = contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: osName, Arch: archName}
|
||||
var host = contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: osName, Arch: archName}
|
||||
|
||||
type rig struct {
|
||||
lib *library
|
||||
@@ -337,7 +337,7 @@ var _ = Describe("Update detection", func() {
|
||||
It("names the host, SDK and data format ranges a candidate does not fit", func() {
|
||||
for reason, requires := range map[string]catalog.Requirements{
|
||||
updates.ReasonHost: {Host: contract.VersionRange{Min: 2, Max: 3}},
|
||||
updates.ReasonAPI: {API: contract.VersionRange{Min: 2, Max: 2}},
|
||||
updates.ReasonAPI: {API: contract.VersionRange{Min: 3, Max: 3}},
|
||||
updates.ReasonProtocol: {Protocol: contract.VersionRange{Min: 2, Max: 2}},
|
||||
updates.ReasonSDK: {SDK: contract.VersionRange{Min: 2, Max: 2}},
|
||||
updates.ReasonDataFormat: {DataFormat: contract.VersionRange{Min: 2, Max: 3}},
|
||||
|
||||
@@ -131,7 +131,7 @@ func newUpdateRig(withUpdates bool) *updateRig {
|
||||
Expect(sources.Save(ctx, actor, catalog.Source{ID: "test", Name: "Test", URL: lib.server.URL + "/library.yaml", Enabled: true, AllowLoopbackHTTP: true})).To(Succeed())
|
||||
c := catalog.Catalog{Sources: sources, TargetOS: "linux", TargetArch: "amd64"}
|
||||
cfg := OwnerConfig{InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "disk-one", Root: filepath.Join(root, "owner")}
|
||||
i := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
i := &packages.Installer{Root: filepath.Join(cfg.Root, "packages"), Catalog: c, Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
||||
DeferCleanup(func() {
|
||||
_ = filepath.Walk(root, func(p string, info os.FileInfo, e error) error {
|
||||
if e == nil && info.IsDir() {
|
||||
|
||||
@@ -341,7 +341,7 @@ 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
|
||||
declares a descriptor must accept API 2 (`"api": {"min": 2, "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
|
||||
@@ -405,7 +405,7 @@ control to an add-on that asks for `network-policy` in `ui.controls`, is granted
|
||||
`egress.check` and declares a tool that replaces `bash`; it names the network
|
||||
by the title of that tool. This is the shape of the manifests written before
|
||||
this field existed. The field is optional and does not change the protocol: the
|
||||
manifest must accept API 2 (`"api": {"min": 1, "max": 2}`), and the host still
|
||||
manifest must accept API 2 (`"api": {"min": 2, "max": 2}`), and the host still
|
||||
accepts add-ons that declare `{1,1}`.
|
||||
|
||||
### Requested host operations are not grants
|
||||
@@ -485,7 +485,7 @@ own record of the grants.
|
||||
|
||||
```json
|
||||
{
|
||||
"api": {"min": 1, "max": 2},
|
||||
"api": {"min": 2, "max": 2},
|
||||
"capabilities": {
|
||||
"resources": {
|
||||
"noun": "board",
|
||||
@@ -2647,7 +2647,7 @@ An add-on declares its sources in the manifest instead, as `capabilities.context
|
||||
|
||||
```json
|
||||
{
|
||||
"api": { "min": 2, "max": 2 },
|
||||
"api": {"min": 2, "max": 2},
|
||||
"capabilities": {
|
||||
"contextFiles": [
|
||||
{ "scope": "user", "path": "Memory/index.md" },
|
||||
|
||||
@@ -4426,7 +4426,7 @@ Expected: a `failed` line: `Cannot find module '../build.mjs'`.
|
||||
"os": "linux",
|
||||
"arch": "amd64",
|
||||
"host": { "min": 1, "max": 1 },
|
||||
"api": { "min": 1, "max": 1 },
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": { "min": 1, "max": 1 },
|
||||
"dataFormat": { "min": 1, "max": 1 },
|
||||
"capabilities": {
|
||||
|
||||
@@ -95,9 +95,6 @@ const (
|
||||
// MinPreviewMs and MaxPreviewMs bound ConnectorKind.PreviewMs.
|
||||
MinPreviewMs = 250
|
||||
MaxPreviewMs = 10000
|
||||
// MinHistoryAPI is the first manifest API version that knows the History
|
||||
// ability of a connector kind.
|
||||
MinHistoryAPI uint64 = 2
|
||||
// DefaultHistoryMessages is how many messages the host asks the op
|
||||
// history for when the kind declares no limit of its own.
|
||||
DefaultHistoryMessages = 30
|
||||
@@ -133,13 +130,12 @@ type ConnectorKind struct {
|
||||
PreviewMs int `json:"previewMs,omitempty"`
|
||||
// History declares that the add-on serves the op history. A kind without
|
||||
// it keeps the behaviour of a platform that cannot be asked for old
|
||||
// messages. It needs manifest api MinHistoryAPI.
|
||||
// messages.
|
||||
History *ConnectorHistory `json:"history,omitempty"`
|
||||
// Options are the choices of the add-on that each binding of this kind
|
||||
// makes, such as how the bot follows a thread. The host shows them next to
|
||||
// the other settings of the binding, keeps the choice, checks it against
|
||||
// this declaration and sends it with the op poll. It needs manifest api
|
||||
// MinHistoryAPI.
|
||||
// this declaration and sends it with the op poll.
|
||||
Options []ConnectorOption `json:"options,omitempty"`
|
||||
// Files declares the files the kind can receive and send, with a limit
|
||||
// for each direction. Without it, the host moves files inline and caps
|
||||
@@ -147,13 +143,11 @@ type ConnectorKind struct {
|
||||
Files *ConnectorFiles `json:"files,omitempty"`
|
||||
// Roles are the roles the kind can give to a sender. The add-on owns
|
||||
// the membership and sends the role IDs with each message; the owner of
|
||||
// a binding maps each role to capabilities. It needs manifest api
|
||||
// MinPermissionsAPI.
|
||||
// a binding maps each role to capabilities.
|
||||
Roles []ConnectorRole `json:"roles,omitempty"`
|
||||
// Permissions are the capabilities (see ConnectorCapabilityNames) that the kind can
|
||||
// grant to a role. A sensitive name may be listed to explain the pairing
|
||||
// path, but it is never granted to a role. It needs manifest api
|
||||
// MinPermissionsAPI.
|
||||
// path, but it is never granted to a role.
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
@@ -261,7 +255,7 @@ type ConnectorCapability struct {
|
||||
Kinds []ConnectorKind `json:"kinds"`
|
||||
}
|
||||
|
||||
func validateConnector(c *ConnectorCapability, ui *UI, api VersionRange) error {
|
||||
func validateConnector(c *ConnectorCapability, ui *UI) error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -284,25 +278,19 @@ func validateConnector(c *ConnectorCapability, ui *UI, api VersionRange) error {
|
||||
return fmt.Errorf("connector previewMs must be 0 or %d to %d", MinPreviewMs, MaxPreviewMs)
|
||||
}
|
||||
if h := k.History; h != nil {
|
||||
if api.Max < MinHistoryAPI {
|
||||
return fmt.Errorf("connector history needs api %d", MinHistoryAPI)
|
||||
}
|
||||
if !h.Thread || h.MaxMessages < 0 || h.MaxMessages > MaxHistoryMessages {
|
||||
return fmt.Errorf("connector history needs thread and maxMessages 0 to %d", MaxHistoryMessages)
|
||||
}
|
||||
}
|
||||
if len(k.Options) > 0 {
|
||||
if api.Max < MinHistoryAPI {
|
||||
return fmt.Errorf("connector options need api %d", MinHistoryAPI)
|
||||
}
|
||||
if err := validateOptions(k.Options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := k.Files.validate(api); err != nil {
|
||||
if err := k.Files.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validatePermissions(k, api); err != nil {
|
||||
if err := validatePermissions(k); err != nil {
|
||||
return err
|
||||
}
|
||||
if k.Page != "" {
|
||||
|
||||
@@ -17,7 +17,7 @@ var _ = Describe("the connector capability", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m, err = ParseManifest(b)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m.Capabilities.UI.SDK = VersionRange{Min: 1, Max: 2}
|
||||
m.Capabilities.UI.SDK = VersionRange{Min: 2, Max: 2}
|
||||
m.Capabilities.UI.Pages = []string{"bot"}
|
||||
m.Capabilities.UI.Navigation = nil
|
||||
m.Capabilities.UI.MainPage = ""
|
||||
@@ -44,18 +44,13 @@ var _ = Describe("the connector capability", func() {
|
||||
})
|
||||
Describe("the history ability", func() {
|
||||
kind := func() *ConnectorKind { return &m.Capabilities.Connector.Kinds[0] }
|
||||
BeforeEach(func() { m.API = VersionRange{Min: 1, Max: 2} })
|
||||
BeforeEach(func() { m.API = VersionRange{Min: 2, Max: 2} })
|
||||
|
||||
It("accepts a thread history with the manifest api 2", func() {
|
||||
kind().History = &ConnectorHistory{Thread: true, MaxMessages: 50}
|
||||
Expect(ValidateManifest(m, host())).To(Succeed())
|
||||
Expect(kind().ServesThreadHistory()).To(BeTrue())
|
||||
})
|
||||
It("refuses it under the manifest api 1", func() {
|
||||
m.API = VersionRange{Min: 1, Max: 1}
|
||||
kind().History = &ConnectorHistory{Thread: true}
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
})
|
||||
It("refuses an ability that says nothing and a limit out of bounds", func() {
|
||||
kind().History = &ConnectorHistory{}
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
@@ -81,17 +76,14 @@ var _ = Describe("the connector capability", func() {
|
||||
return ConnectorOption{ID: "followThreads", Title: "Follow threads", Default: "own",
|
||||
Values: []ConnectorOptionValue{{ID: "own", Title: "Own"}, {ID: "any", Title: "Any"}, {ID: "never", Title: "Never"}}}
|
||||
}
|
||||
BeforeEach(func() { m.API = VersionRange{Min: 1, Max: 2} })
|
||||
BeforeEach(func() { m.API = VersionRange{Min: 2, Max: 2} })
|
||||
|
||||
It("accepts an option with a default among its values", func() {
|
||||
kind().Options = []ConnectorOption{follow()}
|
||||
Expect(ValidateManifest(m, host())).To(Succeed())
|
||||
})
|
||||
It("refuses options under api 1, a bad default, one value, a repeated id and too many", func() {
|
||||
It("refuses a bad default, one value, a repeated id and too many", func() {
|
||||
kind().Options = []ConnectorOption{follow()}
|
||||
m.API = VersionRange{Min: 1, Max: 1}
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
m.API = VersionRange{Min: 1, Max: 2}
|
||||
for _, edit := range []func(*ConnectorOption){
|
||||
func(o *ConnectorOption) { o.Default = "sometimes" },
|
||||
func(o *ConnectorOption) { o.Values = o.Values[:1] },
|
||||
|
||||
@@ -6,10 +6,6 @@ import (
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// MinFilesAPI is the first manifest API version that knows the files ability
|
||||
// of a connector kind. A manifest that declares one must accept it.
|
||||
const MinFilesAPI uint64 = 2
|
||||
|
||||
// Limits of the files ability.
|
||||
const (
|
||||
// MaxDeclaredFileBytes is the largest limit a kind may declare: 1 TiB.
|
||||
@@ -69,13 +65,10 @@ func (a *FileAbility) validate(dir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *ConnectorFiles) validate(api VersionRange) error {
|
||||
func (f *ConnectorFiles) validate() error {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
if api.Max < MinFilesAPI {
|
||||
return fmt.Errorf("a connector kind that declares files needs api %d", MinFilesAPI)
|
||||
}
|
||||
if f.Receive == nil && f.Send == nil {
|
||||
return fmt.Errorf("files must declare receive, send or both")
|
||||
}
|
||||
|
||||
@@ -17,13 +17,12 @@ var _ = Describe("the files ability of a connector kind", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m, err = ParseManifest(b)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m.API = VersionRange{Min: 1, Max: 2}
|
||||
m.API = VersionRange{Min: 2, Max: 2}
|
||||
m.Capabilities.UI = nil
|
||||
m.Capabilities.Connector = &ConnectorCapability{Kinds: []ConnectorKind{{ID: "telegram", Title: "Telegram", Delivery: DeliveryPull}}}
|
||||
})
|
||||
host := func() HostCompatibility {
|
||||
h := compatible()
|
||||
h.API, h.APIMin = HostAPIVersion, HostAPIMin
|
||||
h.SDK = HostSDKVersion
|
||||
return h
|
||||
}
|
||||
@@ -44,12 +43,6 @@ var _ = Describe("the files ability of a connector kind", func() {
|
||||
Expect(ValidateManifest(m, host())).To(Succeed())
|
||||
})
|
||||
|
||||
It("needs a manifest that accepts API 2 to declare files", func() {
|
||||
declare(&ConnectorFiles{Receive: &FileAbility{MaxBytes: 1 << 20}})
|
||||
m.API = VersionRange{Min: 1, Max: 1}
|
||||
Expect(ValidateManifest(m, host())).To(MatchError(ContainSubstring("api 2")))
|
||||
})
|
||||
|
||||
DescribeTable("refuses",
|
||||
func(f *ConnectorFiles) {
|
||||
declare(f)
|
||||
|
||||
@@ -8,10 +8,6 @@ import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// MinContextFilesAPI is the first manifest API version that knows the context
|
||||
// files field. A manifest that declares one must accept it.
|
||||
const MinContextFilesAPI uint64 = 2
|
||||
|
||||
// Limits of the context files an add-on declares.
|
||||
const (
|
||||
// MaxContextFiles is the most files one add-on declares: one for each
|
||||
@@ -105,9 +101,6 @@ func validateContextFiles(m Manifest) error {
|
||||
if len(files) == 0 {
|
||||
return nil
|
||||
}
|
||||
if m.API.Max < MinContextFilesAPI {
|
||||
return fmt.Errorf("context files need api %d", MinContextFilesAPI)
|
||||
}
|
||||
if len(files) > MaxContextFiles {
|
||||
return fmt.Errorf("too many context files: at most %d", MaxContextFiles)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ var _ = Describe("The context files declaration", func() {
|
||||
}
|
||||
host := func() HostCompatibility {
|
||||
h := compatible()
|
||||
h.API, h.APIMin = HostAPIVersion, HostAPIMin
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -32,12 +31,6 @@ var _ = Describe("The context files declaration", func() {
|
||||
Expect(ValidateManifest(withFiles(notes), host())).To(Succeed())
|
||||
})
|
||||
|
||||
It("needs a manifest that accepts API 2 to declare context files", func() {
|
||||
m := withFiles(notes)
|
||||
m.API = VersionRange{Min: 1, Max: 1}
|
||||
Expect(ValidateManifest(m, host())).To(MatchError(ContainSubstring("api 2")))
|
||||
})
|
||||
|
||||
It("refuses a path outside the Memory folder, an unsafe path and a wrong placeholder", func() {
|
||||
for _, bad := range []ContextFile{
|
||||
{Scope: ScopeUser, Path: "Documents/index.md"},
|
||||
|
||||
@@ -8,10 +8,6 @@ import (
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// MinCredentialsAPI is the first manifest API version that knows the
|
||||
// credentials declaration. A manifest that declares one must accept it.
|
||||
const MinCredentialsAPI uint64 = 2
|
||||
|
||||
// Kinds of a credential slot. Only the token exists in this version; a kind
|
||||
// the host does not know is refused at install.
|
||||
const CredentialKindToken = "token"
|
||||
@@ -56,13 +52,10 @@ type CredentialSlot struct {
|
||||
// of the person's binding decides whether it runs at all, whether it waits for
|
||||
// a confirmation, and the host holds it to the list of the binding like a
|
||||
// read. Schema, create and action tools cannot carry a credential.
|
||||
func validateCredentials(c Capabilities, api VersionRange) error {
|
||||
func validateCredentials(c Capabilities) error {
|
||||
if len(c.Credentials) == 0 {
|
||||
return nil
|
||||
}
|
||||
if api.Max < MinCredentialsAPI {
|
||||
return fmt.Errorf("credentials need api %d", MinCredentialsAPI)
|
||||
}
|
||||
if len(c.Credentials) > MaxCredentialSlots {
|
||||
return fmt.Errorf("at most %d credential slots", MaxCredentialSlots)
|
||||
}
|
||||
|
||||
@@ -33,17 +33,12 @@ var _ = Describe("The credential slot of an add-on", func() {
|
||||
host := func() HostCompatibility {
|
||||
h := compatible()
|
||||
h.SDK = 0
|
||||
h.API, h.APIMin = HostAPIVersion, HostAPIMin
|
||||
return h
|
||||
}
|
||||
|
||||
It("is valid with API 2, a connector kind and read tools", func() {
|
||||
Expect(ValidateManifest(manifest(nil), host())).To(Succeed())
|
||||
})
|
||||
It("needs the manifest to accept API 2", func() {
|
||||
m := manifest(func(m *Manifest) { m.API = VersionRange{Min: 1, Max: 1} })
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
})
|
||||
It("needs a connector kind to hold the binding", func() {
|
||||
m := manifest(func(m *Manifest) { m.Capabilities.Connector = nil })
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
|
||||
@@ -6,10 +6,6 @@ import (
|
||||
"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"
|
||||
|
||||
@@ -10,10 +10,9 @@ import (
|
||||
|
||||
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 {
|
||||
manifestWith := func(e *ExecutionDescriptor) Manifest {
|
||||
m, err := ParseManifest(fixture())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m.API = api
|
||||
m.Capabilities = Capabilities{
|
||||
Tools: []string{"run"},
|
||||
Actions: true,
|
||||
@@ -25,19 +24,15 @@ var _ = Describe("The execution descriptor of a commands tool", func() {
|
||||
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 valid for a tool that shadows bash", func() {
|
||||
Expect(ValidateManifest(manifestWith(commands), host())).To(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 := manifestWith(commands)
|
||||
m.Capabilities.ToolDefinitions[0].Shadows = ""
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
for _, bad := range []*ExecutionDescriptor{
|
||||
@@ -46,19 +41,14 @@ var _ = Describe("The execution descriptor of a commands tool", func() {
|
||||
{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())
|
||||
Expect(ValidateManifest(manifestWith(bad), 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
|
||||
return manifestWith(e).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}}}
|
||||
|
||||
@@ -17,12 +17,12 @@ var _ = Describe("Host controls", func() {
|
||||
// Any ID works: the host never looks at it.
|
||||
runner := func(id string, controls ...string) Manifest {
|
||||
return Manifest{SchemaVersion: 1, ID: id, Version: "1.0.0", Executable: "bin/" + id, OS: "linux", Arch: "amd64",
|
||||
Host: VersionRange{Min: 1, Max: 1}, API: VersionRange{Min: 1, Max: 1}, Protocol: VersionRange{Min: 1, Max: 1}, DataFormat: VersionRange{Min: 1, Max: 1},
|
||||
Host: VersionRange{Min: 1, Max: 1}, API: VersionRange{Min: 2, Max: 2}, Protocol: VersionRange{Min: 1, Max: 1}, DataFormat: VersionRange{Min: 1, Max: 1},
|
||||
Capabilities: Capabilities{Actions: true, Tools: []string{"bash"},
|
||||
ToolDefinitions: []ToolDefinition{{Name: "bash", Description: "Run a command.", Kind: ToolKindAction, InputSchema: json.RawMessage(bashSchema), Shadows: "bash"}},
|
||||
UI: &UI{Entry: "ui/main.js", SDK: VersionRange{Min: 2, Max: 2}, Controls: controls}}}
|
||||
}
|
||||
host := HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: HostSDKVersion, DataFormat: 1, OS: "linux", Arch: "amd64"}
|
||||
host := HostCompatibility{Host: 1, API: 2, Protocol: 1, SDK: HostSDKVersion, DataFormat: 1, OS: "linux", Arch: "amd64"}
|
||||
|
||||
It("accepts known controls and refuses an unknown or repeated one", func() {
|
||||
Expect(ValidateManifest(runner("sandbox", ControlNetworkPolicy, ControlToolProviders), host)).To(Succeed())
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package contract
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MinLegacyArgsAPI is the first manifest API version that knows legacyArgs. A
|
||||
// manifest that declares one must accept it.
|
||||
const MinLegacyArgsAPI uint64 = 2
|
||||
|
||||
// Bounds of the legacy argument list of one tool.
|
||||
const (
|
||||
MaxLegacyArgs = 8
|
||||
MaxLegacyArgChars = 64
|
||||
MaxLegacyHintChars = 200
|
||||
)
|
||||
|
||||
// LegacyArgument names an argument that an older version of a tool took and
|
||||
// that the tool refuses now. When a call fails the schema check and carries
|
||||
// it, the host answers with the hint instead of a bare refusal, so a model
|
||||
// that learned the old schema can correct itself. The argument is never
|
||||
// forwarded.
|
||||
type LegacyArgument struct {
|
||||
// Name is the old argument name. The tool's schema must not declare it.
|
||||
Name string `json:"name"`
|
||||
// Hint is one plain sentence on what to send instead, such as "use the
|
||||
// markdown string".
|
||||
Hint string `json:"hint"`
|
||||
}
|
||||
|
||||
// LegacyMessage returns the message for a call whose arguments carry a legacy
|
||||
// argument of the tool d, or "". Arguments that would be refused for another
|
||||
// reason (a forbidden name, bad JSON) get no message.
|
||||
func LegacyMessage(d ToolDefinition, raw json.RawMessage) string {
|
||||
if len(d.LegacyArgs) == 0 || len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if json.Unmarshal(raw, &fields) != nil {
|
||||
return ""
|
||||
}
|
||||
for key := range fields {
|
||||
if ForbiddenToolProperty(key) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
for _, l := range d.LegacyArgs {
|
||||
if _, ok := fields[l.Name]; ok {
|
||||
return fmt.Sprintf("%s is no longer supported; %s", l.Name, l.Hint)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func validateLegacyArgs(d ToolDefinition) error {
|
||||
if len(d.LegacyArgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(d.LegacyArgs) > MaxLegacyArgs {
|
||||
return fmt.Errorf("tool %s: too many legacy arguments", d.Name)
|
||||
}
|
||||
var schema struct {
|
||||
Properties map[string]json.RawMessage `json:"properties"`
|
||||
}
|
||||
_ = json.Unmarshal(d.InputSchema, &schema)
|
||||
seen := map[string]bool{}
|
||||
for _, l := range d.LegacyArgs {
|
||||
if !plainText(l.Name, MaxLegacyArgChars) || !plainText(l.Hint, MaxLegacyHintChars) || ForbiddenToolProperty(l.Name) || seen[l.Name] {
|
||||
return fmt.Errorf("tool %s: invalid legacy argument", d.Name)
|
||||
}
|
||||
if _, ok := schema.Properties[l.Name]; ok {
|
||||
return fmt.Errorf("tool %s: legacy argument %s is in the schema", d.Name, l.Name)
|
||||
}
|
||||
seen[l.Name] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -58,25 +58,18 @@ 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
|
||||
)
|
||||
// HostAPIVersion is the manifest API version of this host. It is the only
|
||||
// one: a manifest declares the range {"min": 2, "max": 2}.
|
||||
const HostAPIVersion uint64 = 2
|
||||
|
||||
// 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
|
||||
// isHostAPI reports whether the range is exactly the manifest API of the host.
|
||||
func (r VersionRange) isHostAPI() bool {
|
||||
return r.Min == HostAPIVersion && r.Max == HostAPIVersion
|
||||
}
|
||||
|
||||
type HostCompatibility struct {
|
||||
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"`
|
||||
Host uint64 `json:"host"`
|
||||
API uint64 `json:"api"`
|
||||
Protocol uint64 `json:"protocol"`
|
||||
SDK uint64 `json:"sdk"`
|
||||
DataFormat uint64 `json:"dataFormat"`
|
||||
@@ -140,8 +133,8 @@ type Capabilities struct {
|
||||
ContextFiles []ContextFile `json:"contextFiles,omitempty"`
|
||||
|
||||
// Credentials declare the secret that the host keeps for the person and
|
||||
// hands to each call (see credentials.go). It needs manifest api
|
||||
// MinCredentialsAPI and a connector kind that holds the binding.
|
||||
// hands to each call (see credentials.go). It needs a connector
|
||||
// kind that holds the binding.
|
||||
Credentials []CredentialSlot `json:"credentials,omitempty"`
|
||||
}
|
||||
|
||||
@@ -378,11 +371,10 @@ 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")
|
||||
}
|
||||
apiMin := h.APIMin
|
||||
if apiMin == 0 || apiMin > h.API {
|
||||
apiMin = h.API
|
||||
if !m.API.isHostAPI() {
|
||||
return fmt.Errorf("the manifest must declare api {\"min\": %d, \"max\": %d}", HostAPIVersion, HostAPIVersion)
|
||||
}
|
||||
if !m.Host.accepts(h.Host) || !m.API.overlaps(apiMin, h.API) || !m.Protocol.accepts(h.Protocol) || !m.DataFormat.accepts(h.DataFormat) {
|
||||
if !m.Host.accepts(h.Host) || !m.API.accepts(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
|
||||
@@ -390,12 +382,6 @@ 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 len(d.LegacyArgs) > 0 && m.API.Max < MinLegacyArgsAPI {
|
||||
return fmt.Errorf("tool %s declares legacy arguments, which need api %d", d.Name, MinLegacyArgsAPI)
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -415,20 +401,15 @@ func ValidateManifest(m Manifest, h HostCompatibility) error {
|
||||
if e := validateResourceTools(c); e != nil {
|
||||
return e
|
||||
}
|
||||
if e := validateCredentials(c, m.API); e != nil {
|
||||
if e := validateCredentials(c); e != nil {
|
||||
return e
|
||||
}
|
||||
if e := validateResourceList(c, m.API); e != nil {
|
||||
if e := validateResourceList(c); e != nil {
|
||||
return e
|
||||
}
|
||||
if e := validateConnector(c.Connector, c.UI, m.API); e != nil {
|
||||
if e := validateConnector(c.Connector, c.UI); e != nil {
|
||||
return e
|
||||
}
|
||||
for _, d := range c.SkillDefinitions {
|
||||
if d.Slot != nil && m.API.Max < MinSlotAPI {
|
||||
return fmt.Errorf("skill %s declares a slot, which needs api %d", d.Name, MinSlotAPI)
|
||||
}
|
||||
}
|
||||
if e := validateSkillDefinitions(m.ID, c.SkillDefinitions); e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ func fixture() []byte {
|
||||
return b
|
||||
}
|
||||
func compatible() HostCompatibility {
|
||||
return HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}
|
||||
return HostCompatibility{Host: 1, API: 2, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}
|
||||
}
|
||||
|
||||
var _ = Describe("Manifest", func() {
|
||||
@@ -362,17 +362,31 @@ var _ = Describe("Resource list declaration", func() {
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
return m
|
||||
}
|
||||
It("accepts a listing declared with API 2", func() {
|
||||
It("accepts only the manifest API of the host, exactly", func() {
|
||||
m := load()
|
||||
m.API = VersionRange{Min: 1, Max: MinResourceListAPI}
|
||||
Expect(ValidateManifest(m, compatible())).To(Succeed())
|
||||
for name, api := range map[string]VersionRange{
|
||||
"absent": {},
|
||||
"api 1": {Min: 1, Max: 1},
|
||||
"1 to 2": {Min: 1, Max: 2},
|
||||
"2 to 3": {Min: 2, Max: 3},
|
||||
"api 3": {Min: 3, Max: 3},
|
||||
"inverted": {Min: 2, Max: 1},
|
||||
} {
|
||||
m.API = api
|
||||
Expect(ValidateManifest(m, compatible())).To(MatchError(ContainSubstring(`"min": 2, "max": 2`)), name)
|
||||
}
|
||||
})
|
||||
It("accepts a declared listing", func() {
|
||||
m := load()
|
||||
m.API = VersionRange{Min: 2, Max: 2}
|
||||
m.Capabilities.Resources.List = &ResourceList{Tool: "things-list", Items: "things"}
|
||||
Expect(ValidateManifest(m, compatible())).To(Succeed())
|
||||
Expect(m.Capabilities.Resources.List.ItemsKey()).To(Equal("things"))
|
||||
Expect((ResourceList{Tool: "x"}).ItemsKey()).To(Equal(DefaultResourceListItems))
|
||||
})
|
||||
It("refuses a listing without API 2, with a bad name, or that names a declared tool", func() {
|
||||
It("refuses a listing with a bad name, or that names a declared tool", func() {
|
||||
for name, mutate := range map[string]func(*Manifest){
|
||||
"api 1": func(m *Manifest) { m.API.Max = 1 },
|
||||
"bad tool": func(m *Manifest) { m.Capabilities.Resources.List.Tool = "Bad Name" },
|
||||
"bad items": func(m *Manifest) { m.Capabilities.Resources.List.Items = "no spaces" },
|
||||
"declared": func(m *Manifest) { m.Capabilities.Resources.List.Tool = m.Capabilities.Tools[0] },
|
||||
@@ -380,7 +394,7 @@ var _ = Describe("Resource list declaration", func() {
|
||||
"empty record": func(m *Manifest) { m.Capabilities.Resources.List.Tool = "" },
|
||||
} {
|
||||
m := load()
|
||||
m.API = VersionRange{Min: 1, Max: MinResourceListAPI}
|
||||
m.API = VersionRange{Min: 2, Max: 2}
|
||||
m.Capabilities.Resources.List = &ResourceList{Tool: "things-list"}
|
||||
mutate(&m)
|
||||
Expect(ValidateManifest(m, compatible())).NotTo(Succeed(), name)
|
||||
|
||||
@@ -8,10 +8,6 @@ import (
|
||||
"github.com/mudler/LocalAI/pkg/egresspolicy"
|
||||
)
|
||||
|
||||
// MinNetworkAPI is the first manifest API version that knows the network
|
||||
// capability. A manifest that declares one must accept it.
|
||||
const MinNetworkAPI uint64 = 2
|
||||
|
||||
// NetworkKindEgressCheck is the one kind of network policy today: the add-on
|
||||
// runs commands behind a proxy that asks the host about each connection with
|
||||
// the host operation egress.check, and the host answers from the
|
||||
@@ -62,9 +58,6 @@ func (n *NetworkCapability) validate(m Manifest) error {
|
||||
if n.Kind != NetworkKindEgressCheck {
|
||||
return fmt.Errorf("unknown network kind")
|
||||
}
|
||||
if m.API.Max < MinNetworkAPI {
|
||||
return fmt.Errorf("a network capability needs api %d", MinNetworkAPI)
|
||||
}
|
||||
if ops := m.Capabilities.HostOperations; ops == nil || !(slices.Contains(ops.Required, egressCheckOp) || slices.Contains(ops.Optional, egressCheckOp)) {
|
||||
return fmt.Errorf("a network capability of kind %s needs the host operation %s", NetworkKindEgressCheck, egressCheckOp)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ var _ = Describe("The network capability", func() {
|
||||
withNetwork := func(n *NetworkCapability, shadowsBash bool) Manifest {
|
||||
m, err := ParseManifest(fixture())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m.API = VersionRange{Min: 1, Max: 2}
|
||||
m.API = VersionRange{Min: 2, Max: 2}
|
||||
m.Capabilities = Capabilities{
|
||||
Service: true,
|
||||
Network: n,
|
||||
@@ -36,7 +36,6 @@ var _ = Describe("The network capability", func() {
|
||||
}
|
||||
host := func() HostCompatibility {
|
||||
h := compatible()
|
||||
h.API, h.APIMin = HostAPIVersion, HostAPIMin
|
||||
h.SDK = HostSDKVersion
|
||||
return h
|
||||
}
|
||||
@@ -47,12 +46,6 @@ var _ = Describe("The network capability", func() {
|
||||
Expect(ValidateManifest(withNetwork(declared, false), host())).To(Succeed())
|
||||
})
|
||||
|
||||
It("needs a manifest that accepts API 2", func() {
|
||||
m := withNetwork(declared, false)
|
||||
m.API = VersionRange{Min: 1, Max: 1}
|
||||
Expect(ValidateManifest(m, host())).To(MatchError(ContainSubstring("api 2")))
|
||||
})
|
||||
|
||||
It("needs the egress check among the host operations", func() {
|
||||
m := withNetwork(declared, false)
|
||||
m.Capabilities.HostOperations = nil
|
||||
|
||||
@@ -6,10 +6,6 @@ import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
// MinPermissionsAPI is the first manifest API version that knows the roles and
|
||||
// permissions of a connector kind.
|
||||
const MinPermissionsAPI uint64 = 2
|
||||
|
||||
// Limits of the permissions of a connector kind.
|
||||
const (
|
||||
MaxConnectorRoles = 8
|
||||
@@ -175,13 +171,10 @@ func (k ConnectorKind) FilterRoles(ids []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func validatePermissions(k ConnectorKind, api VersionRange) error {
|
||||
func validatePermissions(k ConnectorKind) error {
|
||||
if len(k.Roles) == 0 && len(k.Permissions) == 0 {
|
||||
return nil
|
||||
}
|
||||
if api.Max < MinPermissionsAPI {
|
||||
return fmt.Errorf("connector roles and permissions need api %d", MinPermissionsAPI)
|
||||
}
|
||||
if len(k.Roles) > MaxConnectorRoles {
|
||||
return fmt.Errorf("a connector kind has at most %d roles", MaxConnectorRoles)
|
||||
}
|
||||
|
||||
@@ -15,13 +15,12 @@ var _ = Describe("the roles and permissions of a connector kind", func() {
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m, err = ParseManifest(b)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m.API = VersionRange{Min: 1, Max: 2}
|
||||
m.API = VersionRange{Min: 2, Max: 2}
|
||||
m.Capabilities.UI = nil
|
||||
m.Capabilities.Connector = &ConnectorCapability{Kinds: []ConnectorKind{{ID: "chat", Title: "Chat", Delivery: DeliveryPull}}}
|
||||
})
|
||||
host := func() HostCompatibility {
|
||||
h := compatible()
|
||||
h.API, h.APIMin = HostAPIVersion, HostAPIMin
|
||||
h.SDK = HostSDKVersion
|
||||
return h
|
||||
}
|
||||
@@ -33,17 +32,6 @@ var _ = Describe("the roles and permissions of a connector kind", func() {
|
||||
Expect(ValidateManifest(m, host())).To(Succeed())
|
||||
})
|
||||
|
||||
It("accepts a kind that declares neither, on api 1", func() {
|
||||
m.API = VersionRange{Min: 1, Max: 1}
|
||||
Expect(ValidateManifest(m, host())).To(Succeed())
|
||||
})
|
||||
|
||||
It("needs api 2", func() {
|
||||
m.API = VersionRange{Min: 1, Max: 1}
|
||||
kind().Permissions = []string{CapMemoryWrite}
|
||||
Expect(ValidateManifest(m, host())).To(MatchError(ContainSubstring("api 2")))
|
||||
})
|
||||
|
||||
It("refuses an unknown or repeated permission", func() {
|
||||
kind().Permissions = []string{"root.everything"}
|
||||
Expect(ValidateManifest(m, host())).NotTo(Succeed())
|
||||
|
||||
@@ -6,10 +6,6 @@ import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
// MinResourceListAPI is the first manifest API version that knows the
|
||||
// resource listing declaration. A manifest that declares one must accept it.
|
||||
const MinResourceListAPI uint64 = 2
|
||||
|
||||
// MaxListedResources bounds the entries of the listing the host builds. The
|
||||
// answer says when it is cut short.
|
||||
const MaxListedResources = 100
|
||||
@@ -39,14 +35,11 @@ func (l ResourceList) ItemsKey() string {
|
||||
return l.Items
|
||||
}
|
||||
|
||||
func validateResourceList(c Capabilities, api VersionRange) error {
|
||||
func validateResourceList(c Capabilities) error {
|
||||
if c.Resources == nil || c.Resources.List == nil {
|
||||
return nil
|
||||
}
|
||||
l := c.Resources.List
|
||||
if api.Max < MinResourceListAPI {
|
||||
return fmt.Errorf("resources list needs api %d", MinResourceListAPI)
|
||||
}
|
||||
if !namespace.MatchString(l.Tool) || (l.Items != "" && !toolPropertyName.MatchString(l.Items)) {
|
||||
return fmt.Errorf("invalid resources list")
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ var _ = Describe("Skill definitions in a manifest", func() {
|
||||
}
|
||||
|
||||
It("parses skillDefinitions from the manifest", func() {
|
||||
b := []byte(`{"schemaVersion":1,"id":"sample","version":"1.2.3","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true,"skillDefinitions":[{"name":"sample"},{"name":"sample-quickstart"}]}}`)
|
||||
b := []byte(`{"schemaVersion":1,"id":"sample","version":"1.2.3","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true,"skillDefinitions":[{"name":"sample"},{"name":"sample-quickstart"}]}}`)
|
||||
m, e := ParseManifest(b)
|
||||
Expect(e).NotTo(HaveOccurred())
|
||||
Expect(SkillNames(m.Capabilities.SkillDefinitions)).To(Equal([]string{"sample", "sample-quickstart"}))
|
||||
|
||||
@@ -10,10 +10,6 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MinSlotAPI is the first manifest API version that knows the slot
|
||||
// declaration of a skill. A manifest that declares one must accept it.
|
||||
const MinSlotAPI uint64 = 2
|
||||
|
||||
// SlotKindDocuments is the kind of a slot for a document format: the skill
|
||||
// that serves it needs a commands provider that reports isolated execution.
|
||||
const SlotKindDocuments = SkillKindDocuments
|
||||
|
||||
@@ -12,13 +12,11 @@ var _ = Describe("Slots served by skills", func() {
|
||||
host := func() HostCompatibility {
|
||||
h := compatible()
|
||||
h.SDK = 0
|
||||
h.API, h.APIMin = HostAPIVersion, HostAPIMin
|
||||
return h
|
||||
}
|
||||
manifestWith := func(api VersionRange, defs ...SkillDefinition) Manifest {
|
||||
manifestWith := func(defs ...SkillDefinition) Manifest {
|
||||
m, err := ParseManifest(fixture())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
m.API = api
|
||||
m.Capabilities.SkillDefinitions = defs
|
||||
return m
|
||||
}
|
||||
@@ -33,19 +31,15 @@ var _ = Describe("Slots served by skills", func() {
|
||||
})
|
||||
|
||||
It("lets an add-on declare a slot the contract has never heard of", func() {
|
||||
Expect(ValidateManifest(manifestWith(VersionRange{Min: 1, Max: 2}, odt), host())).To(Succeed())
|
||||
Expect(ValidateManifest(manifestWith(odt), host())).To(Succeed())
|
||||
Expect(ShadowedSkills([]SkillDefinition{odt})).To(Equal([]string{"odt"}))
|
||||
Expect(SlotsOf([]SkillDefinition{odt})[0].Library).To(Equal("odfpy"))
|
||||
Expect(IsolatedSkills([]ToolDefinition{{Name: "run", Kind: ToolKindAction, Shadows: "bash", InputSchema: json.RawMessage(`{"type":"object","properties":{"command":{"type":"string"}},"required":["command"],"additionalProperties":false}`)}}, []SkillDefinition{odt})).To(Equal([]string{"odt"}))
|
||||
})
|
||||
|
||||
It("needs the manifest to accept API 2 to declare a slot", func() {
|
||||
Expect(ValidateManifest(manifestWith(VersionRange{Min: 1, Max: 1}, odt), host())).NotTo(Succeed())
|
||||
})
|
||||
|
||||
It("keeps a skill that shadows a default slot and declares nothing working", func() {
|
||||
d := SkillDefinition{Name: "docx", Shadows: "docx"}
|
||||
Expect(ValidateManifest(manifestWith(VersionRange{Min: 1, Max: 1}, d), host())).To(Succeed())
|
||||
Expect(ValidateManifest(manifestWith(d), host())).To(Succeed())
|
||||
Expect(SlotsOf([]SkillDefinition{d})[0].Library).To(Equal("python-docx"))
|
||||
})
|
||||
|
||||
@@ -64,38 +58,3 @@ var _ = Describe("Slots served by skills", func() {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("Legacy arguments of a tool", func() {
|
||||
const schema = `{"type":"object","properties":{"path":{"type":"string"},"markdown":{"type":"string"}},"required":["path"],"additionalProperties":false}`
|
||||
def := func(l ...LegacyArgument) ToolDefinition {
|
||||
return ToolDefinition{Name: "create", Description: "Create.", Kind: ToolKindCreate, InputSchema: json.RawMessage(schema), LegacyArgs: l}
|
||||
}
|
||||
blocks := LegacyArgument{Name: "blocks", Hint: "use the markdown string"}
|
||||
|
||||
It("answers with the hint when a call carries the old argument", func() {
|
||||
Expect(LegacyMessage(def(blocks), json.RawMessage(`{"path":"/a","blocks":[1]}`))).To(Equal("blocks is no longer supported; use the markdown string"))
|
||||
})
|
||||
It("says nothing without the argument, without a declaration or for a forbidden property", func() {
|
||||
Expect(LegacyMessage(def(blocks), json.RawMessage(`{"path":"/a"}`))).To(BeEmpty())
|
||||
Expect(LegacyMessage(def(), json.RawMessage(`{"blocks":1}`))).To(BeEmpty())
|
||||
Expect(LegacyMessage(def(blocks), json.RawMessage(`{"blocks":1,"approved":true}`))).To(BeEmpty())
|
||||
Expect(LegacyMessage(def(blocks), json.RawMessage(`nope`))).To(BeEmpty())
|
||||
})
|
||||
It("is validated: bounded, plain, not in the schema, and API 2", func() {
|
||||
Expect(validateLegacyArgs(def(blocks))).To(Succeed())
|
||||
Expect(validateLegacyArgs(def(LegacyArgument{Name: "markdown", Hint: "x"}))).NotTo(Succeed())
|
||||
Expect(validateLegacyArgs(def(LegacyArgument{Name: "blocks", Hint: "<b>"}))).NotTo(Succeed())
|
||||
Expect(validateLegacyArgs(def(blocks, blocks))).NotTo(Succeed())
|
||||
m, err := ParseManifest(fixture())
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
h := compatible()
|
||||
h.SDK = 0
|
||||
h.API, h.APIMin = HostAPIVersion, HostAPIMin
|
||||
m.Capabilities.Tools = []string{"create"}
|
||||
m.Capabilities.ToolDefinitions = []ToolDefinition{def(blocks)}
|
||||
m.API = VersionRange{Min: 1, Max: 1}
|
||||
Expect(ValidateManifest(m, h)).NotTo(Succeed())
|
||||
m.API = VersionRange{Min: 1, Max: 2}
|
||||
Expect(ValidateManifest(m, h)).To(Succeed())
|
||||
})
|
||||
})
|
||||
@@ -85,13 +85,8 @@ type ToolDefinition struct {
|
||||
// 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).
|
||||
// say about the provider. Only a tool that shadows bash may declare it.
|
||||
Execution *ExecutionDescriptor `json:"execution,omitempty"`
|
||||
// LegacyArgs name arguments that an older version of the tool took and
|
||||
// that it refuses now, with a hint for the model (see LegacyArgument). The
|
||||
// manifest must accept API 2 (see MinLegacyArgsAPI).
|
||||
LegacyArgs []LegacyArgument `json:"legacyArgs,omitempty"`
|
||||
}
|
||||
|
||||
// ReplaceableTool says what a tool that shadows a built-in chat tool must look
|
||||
@@ -261,9 +256,6 @@ func validateToolDefinitionsFor(defs []ToolDefinition, tools []string, credentia
|
||||
if err := d.Execution.validate(d); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateLegacyArgs(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: contract.HostAPIVersion, APIMin: contract.HostAPIMin, Protocol: 1, SDK: contract.HostSDKVersion, DataFormat: 1, OS: os_, Arch: arch}
|
||||
return contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: contract.HostSDKVersion, DataFormat: 1, OS: os_, Arch: arch}
|
||||
}
|
||||
|
||||
type fileEntry struct {
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"id":"bare","version":"1.0.0","executable":"bin/fixture","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["ping"],"ui":{"entry":"ui.js","sdk":{"min":1,"max":2},"pages":["home"],"mainPage":"home","title":"Bare import","description":"Imports React by bare specifier, which the host never resolves."}}}
|
||||
{"schemaVersion":1,"id":"bare","version":"1.0.0","executable":"bin/fixture","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["ping"],"ui":{"entry":"ui.js","sdk":{"min":1,"max":2},"pages":["home"],"mainPage":"home","title":"Bare import","description":"Imports React by bare specifier, which the host never resolves."}}}
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"id":"cssb","version":"1.0.0","executable":"bin/fixture","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["ping"],"ui":{"entry":"ui.js","css":["style.css"],"sdk":{"min":1,"max":2},"pages":["home"],"navigation":["home"],"mainPage":"home","title":"CSS B","description":"A second add-on with the same class names."}}}
|
||||
{"schemaVersion":1,"id":"cssb","version":"1.0.0","executable":"bin/fixture","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["ping"],"ui":{"entry":"ui.js","css":["style.css"],"sdk":{"min":1,"max":2},"pages":["home"],"navigation":["home"],"mainPage":"home","title":"CSS B","description":"A second add-on with the same class names."}}}
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"id":"slots","version":"1.0.0","executable":"bin/fixture","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["ping","summarize"],"ui":{"entry":"ui.js","css":["ui/style.css"],"sdk":{"min":1,"max":2},"pages":["home"],"mainPage":"home","title":{"en":"Slots fixture","it":"Fixture degli slot"},"description":"Contributes to every host slot, with a failing contribution and a style collision.","chatActions":[{"id":"summarize","label":{"en":"Summarize chat","it":"Riassumi la chat"},"icon":"sparkles","placement":"composer","target":{"tool":"summarize"}},{"id":"approve","label":"Review a request","icon":"check-circle","placement":"composer","target":{"component":"ApprovePanel"}},{"id":"inspect","label":"Inspect message","icon":"eye","placement":"message","target":{"component":"InspectPanel"}},{"id":"explode","label":"Break on purpose","icon":"warning","placement":"message","target":{"component":"ExplodePanel"}}],"slots":[{"id":"composer-chip","slot":"chat.composer.actions","component":"ComposerChip"},{"id":"broken-chip","slot":"chat.composer.actions","component":"BrokenChip"},{"id":"message-chip","slot":"chat.message.actions","component":"MessageChip"},{"id":"nav-chip","slot":"app.nav","component":"NavChip"},{"id":"settings-card","slot":"settings.sections","component":"SettingsCard"},{"id":"broken-section","slot":"settings.sections","component":"BrokenSection"}],"renderers":[{"kind":"approval-card","tool":"ping","component":"PingCard"},{"kind":"tool-result","tool":"summarize","component":"SummaryResult"}]}}}
|
||||
{"schemaVersion":1,"id":"slots","version":"1.0.0","executable":"bin/fixture","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["ping","summarize"],"ui":{"entry":"ui.js","css":["ui/style.css"],"sdk":{"min":1,"max":2},"pages":["home"],"mainPage":"home","title":{"en":"Slots fixture","it":"Fixture degli slot"},"description":"Contributes to every host slot, with a failing contribution and a style collision.","chatActions":[{"id":"summarize","label":{"en":"Summarize chat","it":"Riassumi la chat"},"icon":"sparkles","placement":"composer","target":{"tool":"summarize"}},{"id":"approve","label":"Review a request","icon":"check-circle","placement":"composer","target":{"component":"ApprovePanel"}},{"id":"inspect","label":"Inspect message","icon":"eye","placement":"message","target":{"component":"InspectPanel"}},{"id":"explode","label":"Break on purpose","icon":"warning","placement":"message","target":{"component":"ExplodePanel"}}],"slots":[{"id":"composer-chip","slot":"chat.composer.actions","component":"ComposerChip"},{"id":"broken-chip","slot":"chat.composer.actions","component":"BrokenChip"},{"id":"message-chip","slot":"chat.message.actions","component":"MessageChip"},{"id":"nav-chip","slot":"app.nav","component":"NavChip"},{"id":"settings-card","slot":"settings.sections","component":"SettingsCard"},{"id":"broken-section","slot":"settings.sections","component":"BrokenSection"}],"renderers":[{"kind":"approval-card","tool":"ping","component":"PingCard"},{"kind":"tool-result","tool":"summarize","component":"SummaryResult"}]}}}
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"id":"uifixture","version":"2.0.0","executable":"bin/fixture","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["ping"],"ui":{"entry":"ui.js","css":["ui/style.css"],"sdk":{"min":1,"max":2},"pages":["home","boom","dialog"],"navigation":["home","dialog"],"chatActions":["summarize"],"mainPage":"home","title":{"en":"UI fixture","it":"Fixture interfaccia"},"description":"Exercises the add-on UI runtime."}}}
|
||||
{"schemaVersion":1,"id":"uifixture","version":"2.0.0","executable":"bin/fixture","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["ping"],"ui":{"entry":"ui.js","css":["ui/style.css"],"sdk":{"min":1,"max":2},"pages":["home","boom","dialog"],"navigation":["home","dialog"],"chatActions":["summarize"],"mainPage":"home","title":{"en":"UI fixture","it":"Fixture interfaccia"},"description":"Exercises the add-on UI runtime."}}}
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"id":"uifixture","version":"1.0.0","executable":"bin/fixture","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["ping"],"ui":{"entry":"ui.js","css":["ui/style.css"],"sdk":{"min":1,"max":2},"pages":["home","boom","dialog"],"navigation":["home","dialog"],"chatActions":["summarize"],"mainPage":"home","title":{"en":"UI fixture","it":"Fixture interfaccia"},"description":"Exercises the add-on UI runtime."}}}
|
||||
{"schemaVersion":1,"id":"uifixture","version":"1.0.0","executable":"bin/fixture","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"launchMode":"always","capabilities":{"service":true,"tools":["ping"],"ui":{"entry":"ui.js","css":["ui/style.css"],"sdk":{"min":1,"max":2},"pages":["home","boom","dialog"],"navigation":["home","dialog"],"chatActions":["summarize"],"mainPage":"home","title":{"en":"UI fixture","it":"Fixture interfaccia"},"description":"Exercises the add-on UI runtime."}}}
|
||||
@@ -9,10 +9,7 @@
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"api": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
},
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"api": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
},
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"api": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
},
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"host": 3,
|
||||
"api": 1,
|
||||
"api": 2,
|
||||
"protocol": 1,
|
||||
"sdk": 1,
|
||||
"dataFormat": 1,
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
{
|
||||
"host": 1,
|
||||
"api": 1,
|
||||
"api": 2,
|
||||
"protocol": 3,
|
||||
"sdk": 1,
|
||||
"dataFormat": 1,
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
{
|
||||
"host": 1,
|
||||
"api": 1,
|
||||
"api": 2,
|
||||
"protocol": 1,
|
||||
"sdk": 3,
|
||||
"dataFormat": 1,
|
||||
@@ -37,7 +37,7 @@
|
||||
},
|
||||
{
|
||||
"host": 1,
|
||||
"api": 1,
|
||||
"api": 2,
|
||||
"protocol": 1,
|
||||
"sdk": 1,
|
||||
"dataFormat": 3,
|
||||
@@ -46,7 +46,7 @@
|
||||
},
|
||||
{
|
||||
"host": 1,
|
||||
"api": 1,
|
||||
"api": 2,
|
||||
"protocol": 1,
|
||||
"sdk": 1,
|
||||
"dataFormat": 1,
|
||||
@@ -55,7 +55,7 @@
|
||||
},
|
||||
{
|
||||
"host": 1,
|
||||
"api": 1,
|
||||
"api": 2,
|
||||
"protocol": 1,
|
||||
"sdk": 1,
|
||||
"dataFormat": 1,
|
||||
|
||||
@@ -9,10 +9,7 @@
|
||||
"min": 1,
|
||||
"max": 2
|
||||
},
|
||||
"api": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
},
|
||||
"api": {"min": 2, "max": 2},
|
||||
"protocol": {
|
||||
"min": 1,
|
||||
"max": 1
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"id":"sample","version":"1.2.3","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true}}
|
||||
{"schemaVersion":1,"id":"sample","version":"1.2.3","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true}}
|
||||
@@ -1 +1 @@
|
||||
{"schemaVersion":1,"id":"sample","version":"1.2.3","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true}}
|
||||
{"schemaVersion":1,"id":"sample","version":"1.2.3","executable":"bin/sample","os":"linux","arch":"amd64","host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},"capabilities":{"service":true}}
|
||||
@@ -105,16 +105,10 @@ func (l *Library) Add(p Package, effectsDir string) error {
|
||||
if binary == nil {
|
||||
binary = l.exe
|
||||
}
|
||||
// A resource listing is declared from manifest API 2 on. Every other
|
||||
// fixture package keeps API 1.
|
||||
apiMax := 1
|
||||
if res, ok := p.Capabilities["resources"].(map[string]any); ok && res["list"] != nil {
|
||||
apiMax = 2
|
||||
}
|
||||
manifest := map[string]any{
|
||||
"schemaVersion": 1, "id": p.ID, "version": p.Version, "executable": executable,
|
||||
"os": runtime.GOOS, "arch": runtime.GOARCH,
|
||||
"host": map[string]int{"min": 1, "max": 2}, "api": map[string]int{"min": 1, "max": apiMax},
|
||||
"host": map[string]int{"min": 1, "max": 2}, "api": map[string]int{"min": 2, "max": 2},
|
||||
"protocol": map[string]int{"min": 1, "max": 1}, "dataFormat": map[string]int{"min": 1, "max": 2},
|
||||
"launchMode": "always", "capabilities": p.Capabilities,
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (c *Config) AppOptions(role string) ([]config.AppOption, error) {
|
||||
addon := config.AddonConfig{
|
||||
Enabled: true, InstanceID: role, OwnerID: Owner, OwnerAddress: c.OwnerAddress(), StorageID: "disk",
|
||||
Root: node.Root, Transport: transport,
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: runtime.GOOS, Arch: runtime.GOARCH},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: runtime.GOOS, Arch: runtime.GOARCH},
|
||||
}
|
||||
if role == Owner {
|
||||
addon.ListenAddress = node.CarrierAddr
|
||||
|
||||
@@ -50,7 +50,7 @@ func openOwner(root string, lib *cluster.Library, ready time.Duration) (*owner,
|
||||
return nil, err
|
||||
}
|
||||
cat := catalog.Catalog{Sources: sources, TargetOS: goruntime.GOOS, TargetArch: goruntime.GOARCH}
|
||||
host := contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH}
|
||||
host := contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH}
|
||||
installer := &packages.Installer{Root: filepath.Join(root, "packages"), Catalog: cat, Host: host}
|
||||
m, err := addons.NewManager(ctx, addons.OwnerConfig{InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "disk", Root: root}, cat, installer, admin)
|
||||
if err != nil {
|
||||
|
||||
@@ -16,13 +16,13 @@ import (
|
||||
)
|
||||
|
||||
func host() contract.HostCompatibility {
|
||||
return contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}
|
||||
return contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}
|
||||
}
|
||||
|
||||
// manifest builds a manifest with the given ui object. Tools are ping and list.
|
||||
func manifest(ui string) []byte {
|
||||
return []byte(`{"schemaVersion":1,"id":"notes","version":"1.0.0","executable":"bin/x","os":"linux","arch":"amd64",` +
|
||||
`"host":{"min":1,"max":2},"api":{"min":1,"max":1},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},` +
|
||||
`"host":{"min":1,"max":2},"api":{"min": 2, "max": 2},"protocol":{"min":1,"max":1},"dataFormat":{"min":1,"max":2},` +
|
||||
`"capabilities":{"tools":["ping","list"],"ui":` + ui + `}}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ func installer(p published, policy catalog.VerificationPolicy) (*packages.Instal
|
||||
})
|
||||
return &packages.Installer{
|
||||
Root: root, Catalog: c,
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
}, release
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ func newHost(local bool) *host {
|
||||
h.owner = config.AddonConfig{
|
||||
Enabled: true, InstanceID: "owner", OwnerID: "owner", OwnerAddress: owner.OwnerAddress, StorageID: "local",
|
||||
Root: filepath.Join(dir, "addons"), ListenAddress: fmt.Sprintf("127.0.0.1:%d", port), Transport: owner,
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
}
|
||||
if local {
|
||||
h.owner.OwnershipMode = auth.AddonModeLocalOwner
|
||||
|
||||
@@ -101,7 +101,7 @@ func newHost() *host {
|
||||
h.owner = config.AddonConfig{
|
||||
Enabled: true, InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://owner.example", StorageID: "local",
|
||||
Root: root, ListenAddress: "127.0.0.1:0", Transport: ownerCarrier(),
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func newHost(local bool) *host {
|
||||
h.owner = config.AddonConfig{
|
||||
Enabled: true, InstanceID: "owner", OwnerID: "owner", OwnerAddress: owner.OwnerAddress, StorageID: "local",
|
||||
Root: filepath.Join(dir, "addons"), ListenAddress: fmt.Sprintf("127.0.0.1:%d", port), Transport: owner,
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: goruntime.GOOS, Arch: goruntime.GOARCH},
|
||||
}
|
||||
if local {
|
||||
h.owner.OwnershipMode = auth.AddonModeLocalOwner
|
||||
|
||||
@@ -255,7 +255,7 @@ func newAddonsFixture(dir, executable, dataDir string, localOwner bool) (*addons
|
||||
f.owner = config.AddonConfig{
|
||||
Enabled: true, InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://" + address, StorageID: "disk",
|
||||
Root: filepath.Join(dataDir, "addons"), ListenAddress: address, Transport: transport,
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: runtime.GOOS, Arch: runtime.GOARCH},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: runtime.GOOS, Arch: runtime.GOARCH},
|
||||
// A spec checks again within seconds: the interval of the refresh
|
||||
// limit is short here and the limit itself stays on.
|
||||
UpdateRefreshInterval: time.Second,
|
||||
@@ -277,7 +277,7 @@ func localOwnerAddons(dataDir string) (config.AddonConfig, error) {
|
||||
return config.AddonConfig{
|
||||
Enabled: true, InstanceID: "owner", OwnerID: "owner", OwnerAddress: "https://" + address, StorageID: "disk",
|
||||
Root: filepath.Join(dataDir, "addons"), ListenAddress: address, Transport: transport,
|
||||
Host: contract.HostCompatibility{Host: 1, API: 1, Protocol: 1, SDK: 1, DataFormat: 1, OS: runtime.GOOS, Arch: runtime.GOARCH},
|
||||
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: runtime.GOOS, Arch: runtime.GOARCH},
|
||||
OwnershipMode: auth.AddonModeLocalOwner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user