Files
LocalAI/pkg/addons/contract/exactkeys.go
T
Ettore Di Giacinto ad7e9b3261 feat(addons): let a manifest declare the context files the host loads
An add-on that keeps small notes for the person can now name the index
files that the host puts in the prompt of a chat. The declaration is
capabilities.contextFiles: at most one file for each scope (user, agent,
channel), each a path under Memory/ that ends in .md, with an optional
byte cap that can only lower the host's 2 KiB cap.

The field is new, so a manifest that uses it must accept manifest API 2.
The add-on only names paths. The host reads them as the person, so the
declaration gives the add-on no access to files.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-11-03 13:57:28 +00:00

95 lines
2.6 KiB
Go

// SPDX-License-Identifier: MIT
package contract
import (
"encoding/json"
"fmt"
"slices"
"strings"
)
// exactObjectFields checks keys before encoding/json can match struct fields
// case-insensitively. strictJSON retains duplicate, trailing and size/depth bounds.
// Only the immediate object is checked: localized maps keep their locale keys.
func exactObjectFields(b []byte, max int64, keys ...string) (map[string]json.RawMessage, error) {
var fields map[string]json.RawMessage
if err := strictJSON(b, &fields, max); err != nil {
return nil, err
}
for key := range fields {
if !slices.Contains(keys, key) {
return nil, fmt.Errorf("noncanonical or unknown JSON key %q", key)
}
}
return fields, nil
}
// rejectExtensionAliases narrows exact spelling to the new declaration entry
// keys, without changing historical spelling behavior for unrelated fields.
func rejectExtensionAliases(b []byte, keys ...string) error {
var fields map[string]json.RawMessage
if err := strictJSON(b, &fields, DefaultLimits().ManifestBytes); err != nil {
return err
}
for key := range fields {
for _, canonical := range keys {
if key != canonical && strings.EqualFold(key, canonical) {
return fmt.Errorf("noncanonical extension key %q", key)
}
}
}
return nil
}
func (c *Capabilities) UnmarshalJSON(b []byte) error {
if err := rejectExtensionAliases(b, "executionProvider", "hostOperations", "network", "contextFiles"); err != nil {
return err
}
type wire Capabilities
var v wire
if err := strictJSON(b, &v, DefaultLimits().ManifestBytes); err != nil {
return err
}
*c = Capabilities(v)
return nil
}
func (u *UI) UnmarshalJSON(b []byte) error {
if err := rejectExtensionAliases(b, "homeCommands", "modelActions"); err != nil {
return err
}
type wire UI
var v wire
if err := strictJSON(b, &v, DefaultLimits().ManifestBytes); err != nil {
return err
}
*u = UI(v)
return nil
}
func (p *ExecutionProvider) UnmarshalJSON(b []byte) error {
if _, err := exactObjectFields(b, DefaultLimits().ManifestBytes, "tools", "statusTool"); err != nil {
return err
}
type wire ExecutionProvider
var v wire
if err := strictJSON(b, &v, DefaultLimits().ManifestBytes); err != nil {
return err
}
*p = ExecutionProvider(v)
return nil
}
func (o *HostOperations) UnmarshalJSON(b []byte) error {
if _, err := exactObjectFields(b, DefaultLimits().ManifestBytes, "required", "optional"); err != nil {
return err
}
type wire HostOperations
var v wire
if err := strictJSON(b, &v, DefaultLimits().ManifestBytes); err != nil {
return err
}
*o = HostOperations(v)
return nil
}