mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-07 04:53:24 -05:00
Chats and agents can check a PiKVM (kvmd) device and, when a person confirms each call, press the power buttons of the machine it controls and use its keyboard. Read tools: get-info (model, versions, no serial or host name), get-power-state (ATX LEDs) and get-keyboard-state. Write tools: power-action (on, off, reset; off_hard, reset_hard and long_press only with a per-device switch), type-text (bounded, no control characters, refuses key, token and password-list shapes) and send-key (a short allowlist, no Windows or Meta chords). All three declare approval always. The preview reads the power state; apply reads it again and rejects a stale operation. The add-on journals before it sends, never retries and reports unknown when the connection drops. One connection is one device: the address is the single resource of the connection and the one sealed credential holds user:password, split at the first colon. A one-time code is not supported. The add-on speaks only to that address, refuses redirects, uses no proxy and accepts a self-signed certificate only through a #sha256= pin in the address. Typing, the text length and hard power actions are per-device switches that an administrator sets on the add-on page; all start off. Snapshots, mass storage, GPIO and the mouse are left out. The add-on ships a skill, a connection page with a Test button and approval cards, the package files, the library entry and the docs, and joins the package workflow and the case inventory. PiKVM API reference: https://docs.pikvm.org/api/ Assisted-by: Claude:claude-sonnet-5-5 go node
128 lines
5.4 KiB
Go
128 lines
5.4 KiB
Go
// SPDX-License-Identifier: MIT
|
|
package packaging
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/services/addons/hostops"
|
|
"github.com/mudler/LocalAI/pkg/addons/contract"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
// This audit records calls, including the mutation/undo wrappers' operation
|
|
// arguments. It is intentionally independent of deployment prefix grants.
|
|
var officialRequests = map[string]contract.HostOperations{
|
|
"fine-tuning": {Required: strings.Fields("finetune.checkpoints finetune.dataset finetune.export finetune.get finetune.list finetune.start finetune.stop")},
|
|
"quantization": {Required: strings.Fields("quantize.get quantize.import quantize.list quantize.start quantize.stop")},
|
|
"office": {Required: strings.Fields("workspace.export workspace.import")},
|
|
"sandbox": {Required: strings.Fields("workspace.changes workspace.commit workspace.materialize"), Optional: strings.Fields("egress.audit egress.check workspace.admin")},
|
|
"web": {Optional: []string{"workspace.admin"}},
|
|
// The policy of the add-on can be changed by an administrator only; the host answers who is one.
|
|
"home-assistant": {Optional: []string{"workspace.admin"}},
|
|
// The per-device switches of the add-on can be changed by an administrator only.
|
|
"pikvm": {Optional: []string{"workspace.admin"}},
|
|
// A connector moves files only when the person turns files on, so both
|
|
// file operations are optional. Delivery is pull, so neither add-on calls
|
|
// connector.inbound.
|
|
"slack": {Optional: strings.Fields("connector.file.get connector.file.put")},
|
|
"discord": {Optional: strings.Fields("connector.file.get connector.file.put")},
|
|
"telegram": {Optional: strings.Fields("connector.file.get connector.file.put")},
|
|
}
|
|
|
|
// connectorRoot holds the file transfer code that the connector add-ons share. It
|
|
// names its operations by constant, so the audit reads those too.
|
|
const connectorRoot = "addons/internal/connector"
|
|
|
|
var connectorConstants = map[string]string{
|
|
"ConnectorInboundOp": contract.ConnectorInboundOp, "ConnectorFilePutOp": contract.ConnectorFilePutOp, "ConnectorFileGetOp": contract.ConnectorFileGetOp,
|
|
}
|
|
|
|
var _ = Describe("official typed package contracts", func() {
|
|
for _, id := range firstPartyAddons {
|
|
It("audits actual host calls and strict package contract for "+id, func() {
|
|
root := filepath.Join("../../..", "addons", id)
|
|
data, err := os.ReadFile(filepath.Join(root, "manifest.json"))
|
|
Expect(err).NotTo(HaveOccurred())
|
|
m, err := contract.ParseManifest(data)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
want, requests := officialRequests[id]
|
|
if requests {
|
|
Expect(m.Capabilities.HostOperations).To(Equal(&want))
|
|
} else {
|
|
Expect(m.Capabilities.HostOperations).To(BeNil(), "no hostclient calls: do not manufacture requests from policy grants")
|
|
}
|
|
// Collect exact operation literals in production source, including fields
|
|
// passed to jobundo. An unknown operation-shaped literal is not evidence
|
|
// of support; compare independently with the real registered vocabulary.
|
|
var used []string
|
|
roots := []string{root}
|
|
if id == "slack" || id == "telegram" || id == "discord" {
|
|
roots = append(roots, filepath.Join("../../..", connectorRoot))
|
|
}
|
|
for _, root := range roots {
|
|
Expect(filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
f, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
if sel, ok := n.(*ast.SelectorExpr); ok {
|
|
if pkg, ok := sel.X.(*ast.Ident); ok && pkg.Name == "contract" {
|
|
if value, ok := connectorConstants[sel.Sel.Name]; ok {
|
|
used = append(used, value)
|
|
}
|
|
}
|
|
}
|
|
lit, ok := n.(*ast.BasicLit)
|
|
if !ok || lit.Kind != token.STRING {
|
|
return true
|
|
}
|
|
value, err := strconv.Unquote(lit.Value)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
if slices.Contains(hostops.SupportedOperations(), value) {
|
|
used = append(used, value)
|
|
}
|
|
return true
|
|
})
|
|
return nil
|
|
})).To(Succeed())
|
|
}
|
|
declared := append(slices.Clone(want.Required), want.Optional...)
|
|
slices.Sort(used)
|
|
used = slices.Compact(used)
|
|
Expect(used).To(ConsistOf(declared), "update the audited call inventory when production calls change")
|
|
for _, name := range declared {
|
|
Expect(hostops.SupportedOperations()).To(ContainElement(name))
|
|
}
|
|
policyPath, err := filepath.Abs("../../../deploy/addons/host-policy.json")
|
|
Expect(err).NotTo(HaveOccurred())
|
|
grants, err := config.LoadAddonHostPolicy(policyPath)
|
|
Expect(err).NotTo(HaveOccurred())
|
|
summary := hostops.Requests(m.Capabilities.HostOperations, hostops.SupportedOperations(), grants[id])
|
|
Expect(summary.MissingRequired).To(BeEmpty())
|
|
denied := hostops.Requests(m.Capabilities.HostOperations, hostops.SupportedOperations(), nil)
|
|
Expect(denied.Granted).To(BeEmpty(), "requests never self-grant")
|
|
Expect(denied.MissingRequired).To(ConsistOf(want.Required))
|
|
p := newProjectFor(id, map[string][]string{amd: nil})
|
|
layout := p.mustBuild(amd, "1.2.3", "fixture executable")
|
|
Expect(p.verify(layout, amd, "1.2.3")).To(Succeed())
|
|
})
|
|
}
|
|
})
|