From 1834d4fd72fb909b95389b12ff878468182bbf8c Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 3 Nov 2026 16:15:07 +0000 Subject: [PATCH] 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 --- addons/face/manifest.json | 2 +- addons/fine-tuning/manifest.json | 5 +- addons/github/internal/tools/tools_test.go | 2 +- addons/github/manifest.json | 5 +- addons/internal/protocol/extensions_test.go | 2 +- addons/memory/manifest.json | 2 +- addons/office/manifest.json | 26 +----- addons/quantization/manifest.json | 5 +- addons/sandbox/internal/tools/tools_test.go | 1 - addons/sandbox/manifest.json | 2 +- addons/slack/manifest.json | 2 +- addons/studio/manifest.json | 2 +- addons/tables/manifest.json | 5 +- addons/talk/manifest.json | 2 +- addons/telegram/manifest.json | 2 +- addons/voice/manifest.json | 2 +- addons/web/manifest.json | 4 +- core/application/addon_auto_upgrade_test.go | 2 +- core/application/addons_split_test.go | 2 +- core/application/addons_test.go | 2 +- core/application/application.go | 2 +- core/application/execution_binding_test.go | 2 +- core/http/endpoints/addons/apps_test.go | 6 +- .../endpoints/addons/installrefusal_test.go | 4 +- core/http/endpoints/addons/updates_test.go | 4 +- core/http/react-ui/e2e/addons-fixtures.js | 2 +- core/http/route_authmodes_test.go | 2 +- core/http/routes/addons_gate_test.go | 2 +- core/http/routes/features_test.go | 2 +- core/http/routes/library_split_test.go | 2 +- core/services/addons/apps/apps.go | 5 +- core/services/addons/apps/apps_test.go | 2 +- .../addons/apps/legacy_resource_list.go | 37 --------- core/services/addons/apps/resources_test.go | 39 --------- core/services/addons/apps_test.go | 4 +- .../services/addons/gateway/admission_test.go | 2 +- .../addons/gateway/ownerfixture_test.go | 4 +- core/services/addons/manager_test.go | 2 +- core/services/addons/packages/install_test.go | 4 +- core/services/addons/policy/policy_test.go | 2 +- core/services/addons/runtime/supervisor.go | 2 +- .../addons/runtime/supervisor_test.go | 2 +- core/services/addons/tools/adapter.go | 8 +- .../services/addons/tools/definitions_test.go | 2 +- core/services/addons/tools/legacyargs_test.go | 63 --------------- core/services/addons/tools/noaddonid_test.go | 8 +- core/services/addons/tools_test.go | 4 +- core/services/addons/updates/checker_test.go | 4 +- core/services/addons/updates_test.go | 2 +- docs/content/features/addons.md | 8 +- .../plans/2026-10-16-addons-navigation.md | 2 +- pkg/addons/contract/connector.go | 26 ++---- pkg/addons/contract/connector_test.go | 16 +--- pkg/addons/contract/connectorfiles.go | 9 +-- pkg/addons/contract/connectorfiles_test.go | 9 +-- pkg/addons/contract/contextfiles.go | 7 -- pkg/addons/contract/contextfiles_test.go | 7 -- pkg/addons/contract/credentials.go | 9 +-- pkg/addons/contract/credentials_test.go | 5 -- pkg/addons/contract/execution.go | 4 - pkg/addons/contract/execution_test.go | 22 ++---- pkg/addons/contract/hostcontrols_test.go | 4 +- pkg/addons/contract/legacyargs.go | 79 ------------------- pkg/addons/contract/manifest.go | 51 ++++-------- pkg/addons/contract/manifest_test.go | 26 ++++-- pkg/addons/contract/network.go | 7 -- pkg/addons/contract/network_test.go | 9 +-- pkg/addons/contract/permissions.go | 9 +-- pkg/addons/contract/permissions_test.go | 14 +--- pkg/addons/contract/resourcelist.go | 9 +-- pkg/addons/contract/skills_test.go | 2 +- pkg/addons/contract/slots.go | 4 - pkg/addons/contract/slots_test.go | 47 +---------- pkg/addons/contract/tooldefs.go | 10 +-- pkg/addons/packaging/build.go | 2 +- tests/addons/fixture/ui/bare/manifest.json | 2 +- tests/addons/fixture/ui/cssb/manifest.json | 2 +- tests/addons/fixture/ui/slots/manifest.json | 2 +- .../fixture/ui/uifixture-2/manifest.json | 2 +- .../addons/fixture/ui/uifixture/manifest.json | 2 +- tests/addons/fixtures/contracts/app.json | 5 +- tests/addons/fixtures/contracts/chat.json | 5 +- tests/addons/fixtures/contracts/headless.json | 5 +- .../fixtures/contracts/incompatible.json | 12 +-- tests/addons/fixtures/contracts/page.json | 5 +- tests/addons/fixtures/contracts/service.json | 2 +- tests/addons/fixtures/packages/manifest.json | 2 +- tests/addons/foundation/cluster/catalog.go | 8 +- tests/addons/foundation/cluster/server.go | 2 +- tests/addons/foundation/service_test.go | 2 +- tests/addons/hostslots/hostslots_test.go | 4 +- tests/addons/packaging/packaging_test.go | 2 +- tests/addons/recovery/host_test.go | 2 +- tests/addons/tables/application_test.go | 2 +- tests/addons/tablesadmin/host_test.go | 2 +- tests/e2e-ui/addons_fixture.go | 4 +- 96 files changed, 161 insertions(+), 611 deletions(-) delete mode 100644 core/services/addons/apps/legacy_resource_list.go delete mode 100644 core/services/addons/tools/legacyargs_test.go delete mode 100644 pkg/addons/contract/legacyargs.go diff --git a/addons/face/manifest.json b/addons/face/manifest.json index 285557e6ee..2d9167d338 100644 --- a/addons/face/manifest.json +++ b/addons/face/manifest.json @@ -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": { diff --git a/addons/fine-tuning/manifest.json b/addons/fine-tuning/manifest.json index 9f714a9edb..94233ee376 100644 --- a/addons/fine-tuning/manifest.json +++ b/addons/fine-tuning/manifest.json @@ -9,10 +9,7 @@ "min": 1, "max": 1 }, - "api": { - "min": 1, - "max": 1 - }, + "api": {"min": 2, "max": 2}, "protocol": { "min": 1, "max": 1 diff --git a/addons/github/internal/tools/tools_test.go b/addons/github/internal/tools/tools_test.go index 65924e0ccd..d0bf5f019b 100644 --- a/addons/github/internal/tools/tools_test.go +++ b/addons/github/internal/tools/tools_test.go @@ -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 { diff --git a/addons/github/manifest.json b/addons/github/manifest.json index 887170e19a..aa685a9b44 100644 --- a/addons/github/manifest.json +++ b/addons/github/manifest.json @@ -9,10 +9,7 @@ "min": 1, "max": 1 }, - "api": { - "min": 2, - "max": 2 - }, + "api": {"min": 2, "max": 2}, "protocol": { "min": 1, "max": 1 diff --git a/addons/internal/protocol/extensions_test.go b/addons/internal/protocol/extensions_test.go index 86b507a7bc..f4b359ead1 100644 --- a/addons/internal/protocol/extensions_test.go +++ b/addons/internal/protocol/extensions_test.go @@ -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) diff --git a/addons/memory/manifest.json b/addons/memory/manifest.json index 6174ccc8f9..f97ca77991 100644 --- a/addons/memory/manifest.json +++ b/addons/memory/manifest.json @@ -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": { diff --git a/addons/office/manifest.json b/addons/office/manifest.json index 12398f394b..e29a09ff4b 100644 --- a/addons/office/manifest.json +++ b/addons/office/manifest.json @@ -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": [ diff --git a/addons/quantization/manifest.json b/addons/quantization/manifest.json index 319221ba14..7ca6fb0f64 100644 --- a/addons/quantization/manifest.json +++ b/addons/quantization/manifest.json @@ -9,10 +9,7 @@ "min": 1, "max": 1 }, - "api": { - "min": 1, - "max": 1 - }, + "api": {"min": 2, "max": 2}, "protocol": { "min": 1, "max": 1 diff --git a/addons/sandbox/internal/tools/tools_test.go b/addons/sandbox/internal/tools/tools_test.go index a51cd3ff5d..34933da3cf 100644 --- a/addons/sandbox/internal/tools/tools_test.go +++ b/addons/sandbox/internal/tools/tools_test.go @@ -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 { diff --git a/addons/sandbox/manifest.json b/addons/sandbox/manifest.json index 309cce423e..b8a8470842 100644 --- a/addons/sandbox/manifest.json +++ b/addons/sandbox/manifest.json @@ -1,7 +1,7 @@ { "api": { "max": 2, - "min": 1 + "min": 2 }, "arch": "amd64", "capabilities": { diff --git a/addons/slack/manifest.json b/addons/slack/manifest.json index 82b1911891..ab8cd21f7d 100644 --- a/addons/slack/manifest.json +++ b/addons/slack/manifest.json @@ -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": { diff --git a/addons/studio/manifest.json b/addons/studio/manifest.json index aca466ab35..030cd09ad6 100644 --- a/addons/studio/manifest.json +++ b/addons/studio/manifest.json @@ -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": { diff --git a/addons/tables/manifest.json b/addons/tables/manifest.json index d2bab0c1be..7106eb88a7 100644 --- a/addons/tables/manifest.json +++ b/addons/tables/manifest.json @@ -9,10 +9,7 @@ "min": 1, "max": 1 }, - "api": { - "min": 1, - "max": 2 - }, + "api": {"min": 2, "max": 2}, "protocol": { "min": 1, "max": 1 diff --git a/addons/talk/manifest.json b/addons/talk/manifest.json index 58f8452a04..dca2226c31 100644 --- a/addons/talk/manifest.json +++ b/addons/talk/manifest.json @@ -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": { diff --git a/addons/telegram/manifest.json b/addons/telegram/manifest.json index adc9667643..c25e9fbef1 100644 --- a/addons/telegram/manifest.json +++ b/addons/telegram/manifest.json @@ -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": { diff --git a/addons/voice/manifest.json b/addons/voice/manifest.json index ac19090ac5..c00ea2c037 100644 --- a/addons/voice/manifest.json +++ b/addons/voice/manifest.json @@ -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": { diff --git a/addons/web/manifest.json b/addons/web/manifest.json index 9a8e352bf6..2de97dcb64 100644 --- a/addons/web/manifest.json +++ b/addons/web/manifest.json @@ -1,7 +1,7 @@ { "api": { - "max": 1, - "min": 1 + "max": 2, + "min": 2 }, "arch": "amd64", "capabilities": { diff --git a/core/application/addon_auto_upgrade_test.go b/core/application/addon_auto_upgrade_test.go index d486f2b1ef..a2dc62d9e9 100644 --- a/core/application/addon_auto_upgrade_test.go +++ b/core/application/addon_auto_upgrade_test.go @@ -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() { diff --git a/core/application/addons_split_test.go b/core/application/addons_split_test.go index b67302749c..c45cf5f934 100644 --- a/core/application/addons_split_test.go +++ b/core/application/addons_split_test.go @@ -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 = "" diff --git a/core/application/addons_test.go b/core/application/addons_test.go index 5171f4af14..260eb22c97 100644 --- a/core/application/addons_test.go +++ b/core/application/addons_test.go @@ -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 diff --git a/core/application/application.go b/core/application/application.go index e6caa36081..a14656f8c4 100644 --- a/core/application/application.go +++ b/core/application/application.go @@ -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} diff --git a/core/application/execution_binding_test.go b/core/application/execution_binding_test.go index 848755d650..fc41182d97 100644 --- a/core/application/execution_binding_test.go +++ b/core/application/execution_binding_test.go @@ -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()) diff --git a/core/http/endpoints/addons/apps_test.go b/core/http/endpoints/addons/apps_test.go index bbef490d72..ecbf1255fc 100644 --- a/core/http/endpoints/addons/apps_test.go +++ b/core/http/endpoints/addons/apps_test.go @@ -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() { diff --git a/core/http/endpoints/addons/installrefusal_test.go b/core/http/endpoints/addons/installrefusal_test.go index cf5412d55d..2eb5b17a16 100644 --- a/core/http/endpoints/addons/installrefusal_test.go +++ b/core/http/endpoints/addons/installrefusal_test.go @@ -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() { diff --git a/core/http/endpoints/addons/updates_test.go b/core/http/endpoints/addons/updates_test.go index 7e32b9703b..e1f1e693c3 100644 --- a/core/http/endpoints/addons/updates_test.go +++ b/core/http/endpoints/addons/updates_test.go @@ -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() { diff --git a/core/http/react-ui/e2e/addons-fixtures.js b/core/http/react-ui/e2e/addons-fixtures.js index 776dbc76e5..9e3522cb8c 100644 --- a/core/http/react-ui/e2e/addons-fixtures.js +++ b/core/http/react-ui/e2e/addons-fixtures.js @@ -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, } diff --git a/core/http/route_authmodes_test.go b/core/http/route_authmodes_test.go index 1ef61bdf97..0f8791f4ef 100644 --- a/core/http/route_authmodes_test.go +++ b/core/http/route_authmodes_test.go @@ -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...) diff --git a/core/http/routes/addons_gate_test.go b/core/http/routes/addons_gate_test.go index 5b4b3037e8..a6f55e80a6 100644 --- a/core/http/routes/addons_gate_test.go +++ b/core/http/routes/addons_gate_test.go @@ -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) diff --git a/core/http/routes/features_test.go b/core/http/routes/features_test.go index 98f13aa3dd..661328f663 100644 --- a/core/http/routes/features_test.go +++ b/core/http/routes/features_test.go @@ -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)) diff --git a/core/http/routes/library_split_test.go b/core/http/routes/library_split_test.go index eee11b3008..862b1af619 100644 --- a/core/http/routes/library_split_test.go +++ b/core/http/routes/library_split_test.go @@ -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 diff --git a/core/services/addons/apps/apps.go b/core/services/addons/apps/apps.go index b307576a6b..c1e2d35901 100644 --- a/core/services/addons/apps/apps.go +++ b/core/services/addons/apps/apps.go @@ -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 } diff --git a/core/services/addons/apps/apps_test.go b/core/services/addons/apps/apps_test.go index 55db9b365d..1e44460619 100644 --- a/core/services/addons/apps/apps_test.go +++ b/core/services/addons/apps/apps_test.go @@ -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"} diff --git a/core/services/addons/apps/legacy_resource_list.go b/core/services/addons/apps/legacy_resource_list.go deleted file mode 100644 index 6df7648758..0000000000 --- a/core/services/addons/apps/legacy_resource_list.go +++ /dev/null @@ -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"} -} diff --git a/core/services/addons/apps/resources_test.go b/core/services/addons/apps/resources_test.go index 68992323ef..111b6899cb 100644 --- a/core/services/addons/apps/resources_test.go +++ b/core/services/addons/apps/resources_test.go @@ -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()) - } - }) -}) diff --git a/core/services/addons/apps_test.go b/core/services/addons/apps_test.go index f672aaf5b2..631e7eb641 100644 --- a/core/services/addons/apps_test.go +++ b/core/services/addons/apps_test.go @@ -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) diff --git a/core/services/addons/gateway/admission_test.go b/core/services/addons/gateway/admission_test.go index 8af6bdd9de..048d302c27 100644 --- a/core/services/addons/gateway/admission_test.go +++ b/core/services/addons/gateway/admission_test.go @@ -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 { diff --git a/core/services/addons/gateway/ownerfixture_test.go b/core/services/addons/gateway/ownerfixture_test.go index 1fc1347a35..7ac112f700 100644 --- a/core/services/addons/gateway/ownerfixture_test.go +++ b/core/services/addons/gateway/ownerfixture_test.go @@ -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() { diff --git a/core/services/addons/manager_test.go b/core/services/addons/manager_test.go index 303a1d76d4..88a1b5d4b8 100644 --- a/core/services/addons/manager_test.go +++ b/core/services/addons/manager_test.go @@ -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() { diff --git a/core/services/addons/packages/install_test.go b/core/services/addons/packages/install_test.go index b7213d0cc2..5e83095eb8 100644 --- a/core/services/addons/packages/install_test.go +++ b/core/services/addons/packages/install_test.go @@ -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 diff --git a/core/services/addons/policy/policy_test.go b/core/services/addons/policy/policy_test.go index a930336fc9..e3a165cbf4 100644 --- a/core/services/addons/policy/policy_test.go +++ b/core/services/addons/policy/policy_test.go @@ -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() }) diff --git a/core/services/addons/runtime/supervisor.go b/core/services/addons/runtime/supervisor.go index e9136b0bc3..051a76e747 100644 --- a/core/services/addons/runtime/supervisor.go +++ b/core/services/addons/runtime/supervisor.go @@ -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 } diff --git a/core/services/addons/runtime/supervisor_test.go b/core/services/addons/runtime/supervisor_test.go index 90de5ef4dc..0021347d87 100644 --- a/core/services/addons/runtime/supervisor_test.go +++ b/core/services/addons/runtime/supervisor_test.go @@ -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()) diff --git a/core/services/addons/tools/adapter.go b/core/services/addons/tools/adapter.go index 3b0bcfbb07..0478d18d5a 100644 --- a/core/services/addons/tools/adapter.go +++ b/core/services/addons/tools/adapter.go @@ -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) { diff --git a/core/services/addons/tools/definitions_test.go b/core/services/addons/tools/definitions_test.go index 079bcf34b4..89f995f1fc 100644 --- a/core/services/addons/tools/definitions_test.go +++ b/core/services/addons/tools/definitions_test.go @@ -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 diff --git a/core/services/addons/tools/legacyargs_test.go b/core/services/addons/tools/legacyargs_test.go deleted file mode 100644 index 90ac92e767..0000000000 --- a/core/services/addons/tools/legacyargs_test.go +++ /dev/null @@ -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") - }) -}) diff --git a/core/services/addons/tools/noaddonid_test.go b/core/services/addons/tools/noaddonid_test.go index 94cdf35d54..1140e5b51b 100644 --- a/core/services/addons/tools/noaddonid_test.go +++ b/core/services/addons/tools/noaddonid_test.go @@ -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) diff --git a/core/services/addons/tools_test.go b/core/services/addons/tools_test.go index c57721ffe7..341b148726 100644 --- a/core/services/addons/tools_test.go +++ b/core/services/addons/tools_test.go @@ -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) diff --git a/core/services/addons/updates/checker_test.go b/core/services/addons/updates/checker_test.go index 2a15a77815..fea0e41b69 100644 --- a/core/services/addons/updates/checker_test.go +++ b/core/services/addons/updates/checker_test.go @@ -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}}, diff --git a/core/services/addons/updates_test.go b/core/services/addons/updates_test.go index 0b2a7fce6b..7cee601a63 100644 --- a/core/services/addons/updates_test.go +++ b/core/services/addons/updates_test.go @@ -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() { diff --git a/docs/content/features/addons.md b/docs/content/features/addons.md index ca1a661c36..6aa1a7e190 100644 --- a/docs/content/features/addons.md +++ b/docs/content/features/addons.md @@ -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" }, diff --git a/docs/superpowers/plans/2026-10-16-addons-navigation.md b/docs/superpowers/plans/2026-10-16-addons-navigation.md index 8bdaa92d66..2e23b3f96c 100644 --- a/docs/superpowers/plans/2026-10-16-addons-navigation.md +++ b/docs/superpowers/plans/2026-10-16-addons-navigation.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": { diff --git a/pkg/addons/contract/connector.go b/pkg/addons/contract/connector.go index fba032a3da..7c91af86ab 100644 --- a/pkg/addons/contract/connector.go +++ b/pkg/addons/contract/connector.go @@ -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 != "" { diff --git a/pkg/addons/contract/connector_test.go b/pkg/addons/contract/connector_test.go index 9712cca49b..a104e927c3 100644 --- a/pkg/addons/contract/connector_test.go +++ b/pkg/addons/contract/connector_test.go @@ -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] }, diff --git a/pkg/addons/contract/connectorfiles.go b/pkg/addons/contract/connectorfiles.go index dede757f01..b67d0ce454 100644 --- a/pkg/addons/contract/connectorfiles.go +++ b/pkg/addons/contract/connectorfiles.go @@ -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") } diff --git a/pkg/addons/contract/connectorfiles_test.go b/pkg/addons/contract/connectorfiles_test.go index 18db8ec699..fe6d588ebb 100644 --- a/pkg/addons/contract/connectorfiles_test.go +++ b/pkg/addons/contract/connectorfiles_test.go @@ -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) diff --git a/pkg/addons/contract/contextfiles.go b/pkg/addons/contract/contextfiles.go index 51f840381c..f8786fab3f 100644 --- a/pkg/addons/contract/contextfiles.go +++ b/pkg/addons/contract/contextfiles.go @@ -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) } diff --git a/pkg/addons/contract/contextfiles_test.go b/pkg/addons/contract/contextfiles_test.go index f26bf4ca8b..5a9b36548c 100644 --- a/pkg/addons/contract/contextfiles_test.go +++ b/pkg/addons/contract/contextfiles_test.go @@ -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"}, diff --git a/pkg/addons/contract/credentials.go b/pkg/addons/contract/credentials.go index b0d0a6da6a..a64f3c3a4c 100644 --- a/pkg/addons/contract/credentials.go +++ b/pkg/addons/contract/credentials.go @@ -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) } diff --git a/pkg/addons/contract/credentials_test.go b/pkg/addons/contract/credentials_test.go index 9a5b2a345d..ab0376c134 100644 --- a/pkg/addons/contract/credentials_test.go +++ b/pkg/addons/contract/credentials_test.go @@ -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()) diff --git a/pkg/addons/contract/execution.go b/pkg/addons/contract/execution.go index 2b2903b916..082bce3c55 100644 --- a/pkg/addons/contract/execution.go +++ b/pkg/addons/contract/execution.go @@ -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" diff --git a/pkg/addons/contract/execution_test.go b/pkg/addons/contract/execution_test.go index 9d5a12ea71..6d362608b3 100644 --- a/pkg/addons/contract/execution_test.go +++ b/pkg/addons/contract/execution_test.go @@ -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}}} diff --git a/pkg/addons/contract/hostcontrols_test.go b/pkg/addons/contract/hostcontrols_test.go index f1bb683ba3..7c6d31cdf2 100644 --- a/pkg/addons/contract/hostcontrols_test.go +++ b/pkg/addons/contract/hostcontrols_test.go @@ -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()) diff --git a/pkg/addons/contract/legacyargs.go b/pkg/addons/contract/legacyargs.go deleted file mode 100644 index 8be4359511..0000000000 --- a/pkg/addons/contract/legacyargs.go +++ /dev/null @@ -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 -} diff --git a/pkg/addons/contract/manifest.go b/pkg/addons/contract/manifest.go index e8ff4bf7b7..095fc34400 100644 --- a/pkg/addons/contract/manifest.go +++ b/pkg/addons/contract/manifest.go @@ -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 } diff --git a/pkg/addons/contract/manifest_test.go b/pkg/addons/contract/manifest_test.go index 5089cc78d9..2b997f25c7 100644 --- a/pkg/addons/contract/manifest_test.go +++ b/pkg/addons/contract/manifest_test.go @@ -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) diff --git a/pkg/addons/contract/network.go b/pkg/addons/contract/network.go index da31b118d2..f940301da4 100644 --- a/pkg/addons/contract/network.go +++ b/pkg/addons/contract/network.go @@ -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) } diff --git a/pkg/addons/contract/network_test.go b/pkg/addons/contract/network_test.go index 35609bd0c5..9fa352bc6b 100644 --- a/pkg/addons/contract/network_test.go +++ b/pkg/addons/contract/network_test.go @@ -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 diff --git a/pkg/addons/contract/permissions.go b/pkg/addons/contract/permissions.go index acc29712d9..5cf0a77b57 100644 --- a/pkg/addons/contract/permissions.go +++ b/pkg/addons/contract/permissions.go @@ -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) } diff --git a/pkg/addons/contract/permissions_test.go b/pkg/addons/contract/permissions_test.go index dce011210c..ed980803ee 100644 --- a/pkg/addons/contract/permissions_test.go +++ b/pkg/addons/contract/permissions_test.go @@ -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()) diff --git a/pkg/addons/contract/resourcelist.go b/pkg/addons/contract/resourcelist.go index 81e19a37ec..a5c987b0d8 100644 --- a/pkg/addons/contract/resourcelist.go +++ b/pkg/addons/contract/resourcelist.go @@ -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") } diff --git a/pkg/addons/contract/skills_test.go b/pkg/addons/contract/skills_test.go index a7f1da39ac..5ca7cfadc9 100644 --- a/pkg/addons/contract/skills_test.go +++ b/pkg/addons/contract/skills_test.go @@ -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"})) diff --git a/pkg/addons/contract/slots.go b/pkg/addons/contract/slots.go index a561eb7154..42d7ff5a2d 100644 --- a/pkg/addons/contract/slots.go +++ b/pkg/addons/contract/slots.go @@ -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 diff --git a/pkg/addons/contract/slots_test.go b/pkg/addons/contract/slots_test.go index 9dd5877d2f..5059daddfe 100644 --- a/pkg/addons/contract/slots_test.go +++ b/pkg/addons/contract/slots_test.go @@ -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: ""}))).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()) - }) -}) diff --git a/pkg/addons/contract/tooldefs.go b/pkg/addons/contract/tooldefs.go index 1a1c17ba7d..171322f467 100644 --- a/pkg/addons/contract/tooldefs.go +++ b/pkg/addons/contract/tooldefs.go @@ -85,13 +85,8 @@ type ToolDefinition struct { // its add-on name (addon____). 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 diff --git a/pkg/addons/packaging/build.go b/pkg/addons/packaging/build.go index f54be14026..212138a567 100644 --- a/pkg/addons/packaging/build.go +++ b/pkg/addons/packaging/build.go @@ -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 { diff --git a/tests/addons/fixture/ui/bare/manifest.json b/tests/addons/fixture/ui/bare/manifest.json index 4914e321b1..08a8c12150 100644 --- a/tests/addons/fixture/ui/bare/manifest.json +++ b/tests/addons/fixture/ui/bare/manifest.json @@ -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."}}} diff --git a/tests/addons/fixture/ui/cssb/manifest.json b/tests/addons/fixture/ui/cssb/manifest.json index 7f31aaefa9..f94fe8794b 100644 --- a/tests/addons/fixture/ui/cssb/manifest.json +++ b/tests/addons/fixture/ui/cssb/manifest.json @@ -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."}}} diff --git a/tests/addons/fixture/ui/slots/manifest.json b/tests/addons/fixture/ui/slots/manifest.json index 3a8af49eb2..2feb1f9568 100644 --- a/tests/addons/fixture/ui/slots/manifest.json +++ b/tests/addons/fixture/ui/slots/manifest.json @@ -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"}]}}} diff --git a/tests/addons/fixture/ui/uifixture-2/manifest.json b/tests/addons/fixture/ui/uifixture-2/manifest.json index 875b3802f1..20c9f1e6b9 100644 --- a/tests/addons/fixture/ui/uifixture-2/manifest.json +++ b/tests/addons/fixture/ui/uifixture-2/manifest.json @@ -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."}}} diff --git a/tests/addons/fixture/ui/uifixture/manifest.json b/tests/addons/fixture/ui/uifixture/manifest.json index b8710301dd..f88c3e3bfd 100644 --- a/tests/addons/fixture/ui/uifixture/manifest.json +++ b/tests/addons/fixture/ui/uifixture/manifest.json @@ -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."}}} diff --git a/tests/addons/fixtures/contracts/app.json b/tests/addons/fixtures/contracts/app.json index ebc4038f78..acce11a531 100644 --- a/tests/addons/fixtures/contracts/app.json +++ b/tests/addons/fixtures/contracts/app.json @@ -9,10 +9,7 @@ "min": 1, "max": 2 }, - "api": { - "min": 1, - "max": 1 - }, + "api": {"min": 2, "max": 2}, "protocol": { "min": 1, "max": 1 diff --git a/tests/addons/fixtures/contracts/chat.json b/tests/addons/fixtures/contracts/chat.json index 647aa369b0..665deb70f3 100644 --- a/tests/addons/fixtures/contracts/chat.json +++ b/tests/addons/fixtures/contracts/chat.json @@ -9,10 +9,7 @@ "min": 1, "max": 2 }, - "api": { - "min": 1, - "max": 1 - }, + "api": {"min": 2, "max": 2}, "protocol": { "min": 1, "max": 1 diff --git a/tests/addons/fixtures/contracts/headless.json b/tests/addons/fixtures/contracts/headless.json index b4d1788559..ea933e192e 100644 --- a/tests/addons/fixtures/contracts/headless.json +++ b/tests/addons/fixtures/contracts/headless.json @@ -9,10 +9,7 @@ "min": 1, "max": 2 }, - "api": { - "min": 1, - "max": 1 - }, + "api": {"min": 2, "max": 2}, "protocol": { "min": 1, "max": 1 diff --git a/tests/addons/fixtures/contracts/incompatible.json b/tests/addons/fixtures/contracts/incompatible.json index 7fce35c939..9c07afd941 100644 --- a/tests/addons/fixtures/contracts/incompatible.json +++ b/tests/addons/fixtures/contracts/incompatible.json @@ -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, diff --git a/tests/addons/fixtures/contracts/page.json b/tests/addons/fixtures/contracts/page.json index dfffe1f16e..126b91eb49 100644 --- a/tests/addons/fixtures/contracts/page.json +++ b/tests/addons/fixtures/contracts/page.json @@ -9,10 +9,7 @@ "min": 1, "max": 2 }, - "api": { - "min": 1, - "max": 1 - }, + "api": {"min": 2, "max": 2}, "protocol": { "min": 1, "max": 1 diff --git a/tests/addons/fixtures/contracts/service.json b/tests/addons/fixtures/contracts/service.json index 89f0301c0b..da0761f7d3 100644 --- a/tests/addons/fixtures/contracts/service.json +++ b/tests/addons/fixtures/contracts/service.json @@ -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}} diff --git a/tests/addons/fixtures/packages/manifest.json b/tests/addons/fixtures/packages/manifest.json index 89f0301c0b..da0761f7d3 100644 --- a/tests/addons/fixtures/packages/manifest.json +++ b/tests/addons/fixtures/packages/manifest.json @@ -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}} diff --git a/tests/addons/foundation/cluster/catalog.go b/tests/addons/foundation/cluster/catalog.go index c9dc2374b9..9865ad1351 100644 --- a/tests/addons/foundation/cluster/catalog.go +++ b/tests/addons/foundation/cluster/catalog.go @@ -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, } diff --git a/tests/addons/foundation/cluster/server.go b/tests/addons/foundation/cluster/server.go index 1871bf821a..c5c3339fc5 100644 --- a/tests/addons/foundation/cluster/server.go +++ b/tests/addons/foundation/cluster/server.go @@ -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 diff --git a/tests/addons/foundation/service_test.go b/tests/addons/foundation/service_test.go index 34debf00e5..d15ba448e8 100644 --- a/tests/addons/foundation/service_test.go +++ b/tests/addons/foundation/service_test.go @@ -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 { diff --git a/tests/addons/hostslots/hostslots_test.go b/tests/addons/hostslots/hostslots_test.go index a0e5dd7b87..0331b37aab 100644 --- a/tests/addons/hostslots/hostslots_test.go +++ b/tests/addons/hostslots/hostslots_test.go @@ -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 + `}}`) } diff --git a/tests/addons/packaging/packaging_test.go b/tests/addons/packaging/packaging_test.go index e1213c6777..4b8c390f63 100644 --- a/tests/addons/packaging/packaging_test.go +++ b/tests/addons/packaging/packaging_test.go @@ -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 } diff --git a/tests/addons/recovery/host_test.go b/tests/addons/recovery/host_test.go index 1f7156003f..47026c8ff3 100644 --- a/tests/addons/recovery/host_test.go +++ b/tests/addons/recovery/host_test.go @@ -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 diff --git a/tests/addons/tables/application_test.go b/tests/addons/tables/application_test.go index cf84560a53..ce9c8b158f 100644 --- a/tests/addons/tables/application_test.go +++ b/tests/addons/tables/application_test.go @@ -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 } diff --git a/tests/addons/tablesadmin/host_test.go b/tests/addons/tablesadmin/host_test.go index b33fc1c956..439c4d80de 100644 --- a/tests/addons/tablesadmin/host_test.go +++ b/tests/addons/tablesadmin/host_test.go @@ -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 diff --git a/tests/e2e-ui/addons_fixture.go b/tests/e2e-ui/addons_fixture.go index ae560284a8..c095545a4d 100644 --- a/tests/e2e-ui/addons_fixture.go +++ b/tests/e2e-ui/addons_fixture.go @@ -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 }