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:
Ettore Di Giacinto committed 2026-11-03 16:15:07 +00:00
1 parent 7efb20bd1f
commit 1834d4fd72
96 files changed
+161 -611

No files matched your search

+1 -1
View File
@@ -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 -4
View File
@@ -9,10 +9,7 @@
"min": 1,
"max": 1
},
"api": {
"min": 1,
"max": 1
},
"api": {"min": 2, "max": 2},
"protocol": {
"min": 1,
"max": 1
+1 -1
View File
@@ -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 {
+1 -4
View File
@@ -9,10 +9,7 @@
"min": 1,
"max": 1
},
"api": {
"min": 2,
"max": 2
},
"api": {"min": 2, "max": 2},
"protocol": {
"min": 1,
"max": 1
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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": {
+4 -22
View File
@@ -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": [
+1 -4
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
{
"api": {
"max": 2,
"min": 1
"min": 2
},
"arch": "amd64",
"capabilities": {
+1 -1
View File
@@ -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 -1
View File
@@ -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 -4
View File
@@ -9,10 +9,7 @@
"min": 1,
"max": 1
},
"api": {
"min": 1,
"max": 2
},
"api": {"min": 2, "max": 2},
"protocol": {
"min": 1,
"max": 1
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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": {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"api": {
"max": 1,
"min": 1
"max": 2,
"min": 2
},
"arch": "amd64",
"capabilities": {
+1 -1
View File
@@ -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() {
+1 -1
View File
@@ -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 = ""
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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}
+1 -1
View File
@@ -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())
+3 -3
View File
@@ -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() {
+2 -2
View File
@@ -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
View File
@@ -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,
}
+1 -1
View File
@@ -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...)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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
+1 -4
View File
@@ -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
}
+1 -1
View File
@@ -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())
}
})
})
+2 -2
View File
@@ -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() {
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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() })
+1 -1
View File
@@ -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())
+1 -7
View File
@@ -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")
})
})
+3 -5
View File
@@ -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)
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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}},
+1 -1
View File
@@ -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() {
+4 -4
View File
@@ -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": {
+7 -19
View File
@@ -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 != "" {
+4 -12
View File
@@ -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] },
+1 -8
View File
@@ -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")
}
+1 -8
View File
@@ -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)
-7
View File
@@ -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)
}
-7
View File
@@ -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"},
+1 -8
View File
@@ -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)
}
-5
View File
@@ -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())
-4
View File
@@ -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"
+6 -16
View File
@@ -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}}}
+2 -2
View File
@@ -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())
-79
View File
@@ -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
}
+16 -35
View File
@@ -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
}
+20 -6
View File
@@ -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)
-7
View File
@@ -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)
}
+1 -8
View File
@@ -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
+1 -8
View File
@@ -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)
}
+1 -13
View File
@@ -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())
+1 -8
View File
@@ -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")
}
+1 -1
View File
@@ -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"}))
-4
View File
@@ -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
+3 -44
View File
@@ -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())
})
})
+1 -9
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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."}}}
+1 -4
View File
@@ -9,10 +9,7 @@
"min": 1,
"max": 2
},
"api": {
"min": 1,
"max": 1
},
"api": {"min": 2, "max": 2},
"protocol": {
"min": 1,
"max": 1
+1 -4
View File
@@ -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,
+1 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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 -7
View File
@@ -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,
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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 + `}}`)
}
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
}