Files
LocalAI/pkg/addons/contract/credentials_test.go
T
Ettore Di Giacinto 2a491cb2cf refactor(addons): remove the shims for older add-on data
No release carried add-ons, so nothing reads older add-on data or
manifests. Delete the code that did.

- Connectors: the kind and the add-on of the Telegram connectors that
  core once carried are gone (LegacyAddon, KindTelegram, the stored
  binding normalization, the platform titles). A binding needs a kind,
  and its add-on is the one that serves the kind. The conversion of the
  old agent pool lists every connector as dropped instead of making a
  Telegram binding, and its notes say so.
- The library index reads only the id field of an entry, not addonId.
- A host control that has a capability block (the network policy) is
  attached by that block alone, not by a tool that replaces bash.
- The Office create tools no longer answer for the argument names of an
  earlier schema, and the identity file note about old addresses is
  gone.
- The known debt of the provider-name guard drops the entries of the
  conversion and of the resource listing shim, and its ceiling goes
  from 13 to 5.

Assisted-by: Claude:claude-sonnet-5-5 go test
2026-11-03 16:56:49 +00:00

186 lines
8.5 KiB
Go

// SPDX-License-Identifier: MIT
package contract
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("The credential slot of an add-on", func() {
const schema = `{"type":"object","properties":{"repo":{"type":"string"}},"required":["repo"],"additionalProperties":false}`
slot := CredentialSlot{ID: "token", Title: "Access token", Kind: CredentialKindToken, ResourceArgument: "repo"}
manifest := func(mod func(*Manifest)) Manifest {
m, err := ParseManifest(fixture())
Expect(err).NotTo(HaveOccurred())
m.API = VersionRange{Min: 2, Max: 2}
m.Capabilities = Capabilities{
Tools: []string{"read-file"},
ToolDefinitions: []ToolDefinition{{Name: "read-file", Description: "Read a file.", Kind: ToolKindRead, InputSchema: json.RawMessage(schema)}},
Credentials: []CredentialSlot{slot},
Connector: &ConnectorCapability{Kinds: []ConnectorKind{{ID: "sample", Title: "Sample", Delivery: DeliveryPull}}},
}
if mod != nil {
mod(&m)
}
return m
}
host := func() HostCompatibility {
h := compatible()
h.SDK = 0
return h
}
It("is valid with a connector kind and read tools", func() {
Expect(ValidateManifest(manifest(nil), host())).To(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())
})
It("is refused with a bad kind, a repeated or extra slot or a bad argument name", func() {
for _, mod := range []func(*Manifest){
func(m *Manifest) { m.Capabilities.Credentials[0].Kind = "app" },
func(m *Manifest) { m.Capabilities.Credentials[0].ID = "Token" },
func(m *Manifest) { m.Capabilities.Credentials[0].Title = "" },
func(m *Manifest) { m.Capabilities.Credentials[0].ResourceArgument = "re po" },
func(m *Manifest) {
m.Capabilities.Credentials = append(m.Capabilities.Credentials, CredentialSlot{ID: "other", Title: "Other", Kind: CredentialKindToken})
},
} {
Expect(ValidateManifest(manifest(mod), host())).NotTo(Succeed())
}
})
Describe("write tools", func() {
const writeSchema = `{"type":"object","properties":{"repo":{"type":"string"},"body":{"type":"string"}},"required":["repo","body"],"additionalProperties":false}`
withWrite := func(schema, approval string) Manifest {
return manifest(func(m *Manifest) {
m.Capabilities.Tools = append(m.Capabilities.Tools, "comment")
m.Capabilities.ToolDefinitions = append(m.Capabilities.ToolDefinitions, ToolDefinition{Name: "comment", Description: "Comment.", Kind: ToolKindWrite,
InputSchema: json.RawMessage(schema), Approval: approval})
})
}
It("are allowed when they name the resource with the slot argument", func() {
Expect(ValidateManifest(withWrite(writeSchema, ""), host())).To(Succeed())
Expect(ValidateManifest(withWrite(writeSchema, ApprovalAlways), host())).To(Succeed())
})
It("never take the host resource argument", func() {
s := `{"type":"object","properties":{"repo":{"type":"string"},"resourceId":{"type":"string"}},"required":["repo","resourceId"],"additionalProperties":false}`
Expect(ValidateManifest(withWrite(s, ""), host())).NotTo(Succeed())
})
It("must require the slot argument", func() {
s := `{"type":"object","properties":{"body":{"type":"string"}},"required":["body"],"additionalProperties":false}`
Expect(ValidateManifest(withWrite(s, ""), host())).NotTo(Succeed())
})
It("refuse an approval value the host does not know", func() {
Expect(ValidateManifest(withWrite(writeSchema, "sometimes"), host())).NotTo(Succeed())
})
It("are the only change a credential allows: schema, create and action tools are refused", func() {
for _, kind := range []string{ToolKindSchema, ToolKindCreate, ToolKindAction} {
m := manifest(func(m *Manifest) {
m.Capabilities.Actions = true
m.Capabilities.Tools = append(m.Capabilities.Tools, "x")
m.Capabilities.ToolDefinitions = append(m.Capabilities.ToolDefinitions, ToolDefinition{Name: "x", Description: "X.", Kind: kind, InputSchema: json.RawMessage(writeSchema)})
})
Expect(ValidateManifest(m, host())).NotTo(Succeed(), kind)
}
})
It("are refused in an add-on without a credential when they lack resourceId", func() {
m := manifest(func(m *Manifest) {
m.Capabilities.Credentials = nil
m.Capabilities.Tools = append(m.Capabilities.Tools, "comment")
m.Capabilities.ToolDefinitions = append(m.Capabilities.ToolDefinitions, ToolDefinition{Name: "comment", Description: "Comment.", Kind: ToolKindWrite, InputSchema: json.RawMessage(writeSchema)})
})
Expect(ValidateManifest(m, host())).NotTo(Succeed())
})
It("accept the approval field on a write tool only", func() {
m := manifest(func(m *Manifest) { m.Capabilities.ToolDefinitions[0].Approval = ApprovalAlways })
Expect(ValidateManifest(m, host())).NotTo(Succeed())
})
It("may be checked on their own with ValidateCredentialedToolSchema", func() {
Expect(ValidateCredentialedToolSchema(json.RawMessage(writeSchema), ToolKindWrite)).To(Succeed())
Expect(ValidateToolSchema(json.RawMessage(writeSchema), ToolKindWrite)).NotTo(Succeed())
})
})
It("makes a tool require the resource argument as a string", func() {
for _, s := range []string{
`{"type":"object","properties":{"repo":{"type":"string"}},"additionalProperties":false}`,
`{"type":"object","properties":{"repo":{"type":"integer"}},"required":["repo"],"additionalProperties":false}`,
`{"type":"object","properties":{"repo":{"type":"string"},"resourceId":{"type":"string"}},"required":["repo","resourceId"],"additionalProperties":false}`,
} {
m := manifest(func(m *Manifest) { m.Capabilities.ToolDefinitions[0].InputSchema = json.RawMessage(s) })
Expect(ValidateManifest(m, host())).NotTo(Succeed(), s)
}
})
It("round trips through the manifest and rejects a spelling of another case", func() {
m := manifest(nil)
b, err := json.Marshal(m.Capabilities)
Expect(err).NotTo(HaveOccurred())
var back Capabilities
Expect(json.Unmarshal(b, &back)).To(Succeed())
Expect(back.Credentials).To(Equal(m.Capabilities.Credentials))
})
Describe("CredentialResource", func() {
def := ToolDefinition{Name: "read-file", Kind: ToolKindRead, InputSchema: json.RawMessage(schema)}
It("returns the key of the argument", func() {
k, found, err := slot.CredentialResource(def, json.RawMessage(`{"repo":"acme/widgets"}`))
Expect(err).NotTo(HaveOccurred())
Expect([]any{k, found}).To(Equal([]any{"acme/widgets", true}))
})
It("refuses a missing or non-string key", func() {
for _, a := range []string{`{}`, `{"repo":""}`, `{"repo":3}`, `[]`} {
_, found, err := slot.CredentialResource(def, json.RawMessage(a))
Expect(found).To(BeTrue())
Expect(err).To(HaveOccurred(), a)
}
})
It("finds nothing in a tool that does not declare the argument", func() {
other := ToolDefinition{Name: "x", Kind: ToolKindRead, InputSchema: json.RawMessage(`{"type":"object","properties":{},"additionalProperties":false}`)}
_, found, err := slot.CredentialResource(other, json.RawMessage(`{}`))
Expect([]any{found, err}).To(Equal([]any{false, nil}))
})
})
Describe("the request", func() {
now := time.Date(2029, 1, 1, 0, 0, 0, 0, time.UTC)
It("carries a resource key and a credential", func() {
v := envelope()
v.Request.ResourceKey, v.Request.Credential = "acme/widgets", "secret"
Expect(v.Validate(now, DefaultLimits())).To(Succeed())
b, err := json.Marshal(v)
Expect(err).NotTo(HaveOccurred())
var back Envelope
Expect(json.Unmarshal(b, &back)).To(Succeed())
Expect(back.Request.Credential).To(Equal("secret"))
})
It("refuses a resource key with a control character or too long, and a huge credential", func() {
for _, change := range []func(*Envelope){
func(v *Envelope) { v.Request.ResourceKey = "a\nb" },
func(v *Envelope) { v.Request.ResourceKey = strings.Repeat("a", MaxResourceKeyBytes+1) },
func(v *Envelope) { v.Request.Credential = strings.Repeat("a", MaxCredentialBytes+1) },
} {
v := envelope()
change(&v)
Expect(v.Validate(now, DefaultLimits())).NotTo(Succeed())
}
})
})
Describe("ErrRateLimited", func() {
It("is found through wrapping and keeps its wait in bounds", func() {
err := fmt.Errorf("call: %w", &ErrRateLimited{RetryAfter: 42})
var rl *ErrRateLimited
Expect(errors.As(err, &rl)).To(BeTrue())
Expect(rl.Seconds()).To(Equal(42))
Expect((&ErrRateLimited{}).Seconds()).To(Equal(1))
Expect((&ErrRateLimited{RetryAfter: 1 << 30}).Seconds()).To(Equal(MaxRetryAfter))
})
})
})