Files
LocalAI/core/application/addon_hostgrants.go
T
Ettore Di Giacinto cbec432ed6 feat(addons): upload a zip to the owner and install it from the Add-ons API
POST /api/addons/files streams the zip to the owner in chunks over the
host access call, so a frontend of a split deployment keeps no copy and
the owner enforces the size limit while it receives. GET and DELETE read
and remove a staged file. The install is the existing host access
preview and install with sourceId local-file, so it keeps the human
surface rule, the nonce and the digest binding. The install body gains a
downgrade confirmation.

Assisted-by: Claude:claude-sonnet-5-5 go gofmt
2026-11-03 22:52:50 +00:00

654 lines
23 KiB
Go

// SPDX-License-Identifier: MIT
package application
import (
"context"
"errors"
"fmt"
"path/filepath"
"regexp"
"slices"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/mudler/LocalAI/core/http/auth"
"github.com/mudler/LocalAI/core/services/addons"
"github.com/mudler/LocalAI/core/services/addons/gateway"
"github.com/mudler/LocalAI/core/services/addons/hostgrants"
"github.com/mudler/LocalAI/core/services/addons/hostops"
"github.com/mudler/LocalAI/pkg/addons/contract"
"github.com/mudler/xlog"
)
// File names of the owner's host grants, beside the host settings.
const (
addonHostGrantsFile = "host-grants.json"
addonHostGrantsAudit = "host-grants-audit.jsonl"
)
// errHostGrantsLocked is what a write answers when the deployment keeps the
// policy file and flags the only source of grants.
var errHostGrantsLocked = errors.New("host grants are locked by the deployment")
// addonHostGrants is the owner's side of the UI host grants: the document, its
// audit file, the deployment floor and the glue to the add-on manager. Only
// the owner has one. A frontend never stores or enforces grants.
type addonHostGrants struct {
floor hostgrants.Floor
locked bool
// lockedReason is gateway.LockedByDeployment or gateway.LockedNoAuth.
lockedReason string
override string
store *hostgrants.Store
storeErr error
audit *hostgrants.Audit
mu sync.RWMutex
manager *addons.Manager
// refresh coalesces the calls that follow a grant write.
refreshMu sync.Mutex
refreshQueued bool
refreshRunning bool
warned atomic.Bool
}
var supportedOps = func() map[string]bool {
m := map[string]bool{}
for _, op := range hostops.SupportedOperations() {
m[op] = true
}
return m
}()
// hostGrantsFloor builds the floor of the deployment: the grants and deny
// rules of the policy file and flags, read once at startup. Empty prefixes are
// dropped, since an empty prefix would match every operation.
func hostGrantsFloor(grants, deny map[string][]string) hostgrants.Floor {
clean := func(in map[string][]string) map[string][]string {
out := map[string][]string{}
for id, list := range in {
for _, e := range list {
if e != "" {
out[id] = append(out[id], e)
}
}
}
return out
}
return hostgrants.Floor{Grants: clean(grants), Deny: clean(deny)}
}
func (a *Application) hostGrantsFloorFromConfig() hostgrants.Floor {
if a.applicationConfig == nil {
return hostgrants.Floor{}
}
return hostGrantsFloor(a.applicationConfig.AddonHostOps, a.applicationConfig.AddonHostDeny)
}
// openAddonHostGrants opens the document and the audit file under the owner's
// state directory. Call it on the owner only, after NewManager took the
// storage guard and before the Authority is first used. A missing document is
// the first start: it is not created until the first write. An unreadable one
// does not stop the owner; the Authority then refuses every operation, because
// the deny rules in it cannot be read.
func (a *Application) openAddonHostGrants(root string) {
cfg := a.applicationConfig
g := &addonHostGrants{floor: a.hostGrantsFloorFromConfig(), override: cfg.AddonHostBlanket}
switch {
case cfg.AddonHostUIGrantsLocked:
g.locked, g.lockedReason = true, gateway.LockedByDeployment
case a.AddonOwnershipMode() == auth.AddonModeLocalOwner && !cfg.AddonHostUIGrantsWithoutAuth:
// Without sign-in every caller that reaches the port is the same
// administrator, so a UI write proves nothing about who made it.
g.locked, g.lockedReason = true, gateway.LockedNoAuth
}
g.store, g.storeErr = hostgrants.Open(filepath.Join(root, addonHostGrantsFile))
if g.storeErr == nil {
var err error
if g.audit, err = hostgrants.OpenAudit(filepath.Join(root, addonHostGrantsAudit)); err != nil {
// Without the audit file nothing may be granted: the store is shut
// so the Authority fails closed, like for an unreadable document.
_ = g.store.Close()
g.store, g.storeErr = nil, fmt.Errorf("%w: audit file", hostgrants.ErrUnavailable)
}
}
a.addonGrants = g
g.logStartup(cfg.AddonHostOpsSource)
switch {
case g.lockedReason == gateway.LockedNoAuth:
xlog.Warn("Addon host grants are locked because sign-in is off: anyone who can reach this port could change them. Set them in the policy file, turn sign-in on, or pass --addon-host-ui-grants-without-auth to allow UI changes knowingly")
case a.AddonOwnershipMode() == auth.AddonModeLocalOwner && !g.locked:
xlog.Warn("Addon host grants can be changed from the UI although sign-in is off: anyone who can reach this port can grant host operations, set deny rules and turn the blanket approval on")
}
if g.storeErr == nil {
a.addonGrantStore = g
}
}
func (g *addonHostGrants) logStartup(source string) {
if source == "" {
source = "empty"
}
prefixes, denyRules := 0, 0
for _, l := range g.floor.Grants {
prefixes += len(l)
}
for _, l := range g.floor.Deny {
denyRules += len(l)
}
mode, byDeployment := hostgrants.BlanketOff, false
if g.override != "" {
mode, byDeployment = g.override, true
}
fields := []any{"floorSource", source, "floorAddons", len(g.floor.Grants), "floorPrefixes", prefixes, "floorDenyRules", denyRules, "uiGrants", map[bool]string{true: "locked", false: "enabled"}[g.locked]}
if g.storeErr != nil {
xlog.Warn("Addon host grants store is unreadable: no add-on can call a host operation until it is repaired", "reason", describeStoreError(g.storeErr))
xlog.Info("Addon host grants", fields...)
return
}
snap, _ := g.store.Snapshot(context.Background())
ops := 0
for _, e := range snap.Grants {
ops += len(e.Ops)
}
if !byDeployment {
mode = snap.Blanket.Mode
}
uiDeny := 0
for _, l := range snap.Deny {
uiDeny += len(l)
}
fields = append(fields, "revision", snap.Revision, "uiGrantAddons", len(snap.Grants), "uiGrantOperations", ops, "uiDenyRules", uiDeny, "blanket", mode, "blanketSetByDeployment", byDeployment)
xlog.Info("Addon host grants", fields...)
if g.locked && len(snap.Grants)+uiDeny > 0 {
xlog.Warn("Addon host grants are locked: the document keeps applying as it is, and no UI write changes it")
}
}
// describeStoreError gives a reason that names no path.
func describeStoreError(err error) string {
switch {
case errors.Is(err, hostgrants.ErrInvalid):
return "the document is not valid"
case errors.Is(err, hostgrants.ErrUncertain):
return "the last write is in doubt"
case errors.Is(err, hostgrants.ErrUnavailable):
return "the document cannot be opened or locked"
}
return "unreadable"
}
func (a *Application) closeAddonHostGrants() {
if g := a.addonGrants; g != nil {
if g.store != nil {
_ = g.store.Close()
}
_ = g.audit.Close()
}
}
func (g *addonHostGrants) bind(m *addons.Manager) {
g.mu.Lock()
defer g.mu.Unlock()
g.manager = m
}
func (g *addonHostGrants) mgr() *addons.Manager {
g.mu.RLock()
defer g.mu.RUnlock()
return g.manager
}
// context is what the evaluator needs for one add-on. Until the manager is
// bound no identity is known, so no UI entry counts.
func (g *addonHostGrants) context(id string) hostgrants.Context {
c := hostgrants.Context{Registered: func(op string) bool { return supportedOps[op] }}
if m := g.mgr(); m != nil {
c.Identity, c.Completed = m.TrustedIdentity(id), m.OperationCompleted
} else {
c.Completed = func(string) bool { return false }
}
return c
}
// failClosed wraps the Authority so that a store that cannot be read refuses
// every operation: the UI deny rules in it are unknown, and an empty view would
// let a denied operation through.
type failClosed struct {
inner hostops.Authority
g *addonHostGrants
}
func (f failClosed) broken() bool {
if f.g.storeErr != nil || (f.g.store != nil && f.g.store.Usable() != nil) {
if f.g.warned.CompareAndSwap(false, true) {
xlog.Error("Addon host grants store is unreadable: every host operation is refused")
}
return true
}
return false
}
func (f failClosed) Allows(id, op string) (bool, hostops.Reason) {
if id == "" || f.broken() {
return false, hostops.ReasonNotGranted
}
return f.inner.Allows(id, op)
}
func (f failClosed) Snapshot(id string) hostops.EffectiveSet {
if id == "" || f.broken() {
return hostops.EffectiveSet{}
}
return f.inner.Snapshot(id)
}
func (f failClosed) Entitled(id string) bool {
return id != "" && !f.broken() && f.inner.Entitled(id)
}
// buildAddonAuthority is the Authority every reader asks. On the owner it reads
// the store on every call; elsewhere it is the floor of the deployment alone.
func (a *Application) buildAddonAuthority() hostops.Authority {
g := a.addonGrants
if g == nil {
return hostgrants.NewAuthority(hostgrants.AuthorityConfig{Floor: a.hostGrantsFloorFromConfig()})
}
inner := hostgrants.NewAuthority(hostgrants.AuthorityConfig{Floor: g.floor, Store: g.store, Context: g.context})
return failClosed{inner: inner, g: g}
}
// AddonHostGrantsStore is the owner's document of UI grants, or nil in a process that has none.
func (a *Application) AddonHostGrantsStore() *hostgrants.Store {
if a.addonGrants == nil {
return nil
}
return a.addonGrants.store
}
// AddonHostGrantsAfterChange is the hook for every grant write. It asks the
// manager to look again at what each add-on holds, in the background, and never
// blocks. The Authority needs no call: it reads the store on every operation.
func (a *Application) AddonHostGrantsAfterChange(addonIDs []string) {
if g := a.addonGrants; g != nil {
g.refreshSoon(a.applicationConfig.Context, addonIDs)
}
}
// ApplyAddonHostGrants runs the refresh now.
func (a *Application) ApplyAddonHostGrants(ctx context.Context) error {
if a.addonGrants == nil || a.addonGrants.mgr() == nil {
return addons.ErrConfiguration
}
return a.addonGrants.mgr().HostGrantsChanged(ctx)
}
func (g *addonHostGrants) refreshSoon(ctx context.Context, ids []string) {
m := g.mgr()
if m == nil {
return
}
g.refreshMu.Lock()
g.refreshQueued = true
if g.refreshRunning {
g.refreshMu.Unlock()
return
}
g.refreshRunning = true
g.refreshMu.Unlock()
go func() {
for {
g.refreshMu.Lock()
if !g.refreshQueued {
g.refreshRunning = false
g.refreshMu.Unlock()
return
}
g.refreshQueued = false
g.refreshMu.Unlock()
if ctx == nil || ctx.Err() == nil {
started, stopped, err := m.HostGrantsChangedReport(context.Background())
if err != nil && !errors.Is(err, addons.ErrRecoveryRequired) {
xlog.Warn("Could not apply a change of host grants to the add-ons", "addons", ids, "error", err)
}
for _, id := range started {
g.auditAppend(hostgrants.AuditEntry{Event: hostgrants.AuditRestartForGrant, AddonID: id, Reason: "started_after_grant"})
}
for _, id := range stopped {
g.auditAppend(hostgrants.AuditEntry{Event: hostgrants.AuditRestartForGrant, AddonID: id, Reason: "stopped_after_access_removed"})
}
}
}
}()
}
// --- addons.GrantStore ---
var blanketName = regexp.MustCompile(`^blanket:(official|any)@rev([0-9]+)$`)
func (g *addonHostGrants) now() string { return time.Now().UTC().Format(time.RFC3339) }
// auditAppend records a refusal or a housekeeping removal. A failure is logged
// by the caller's error path only: nothing was granted, so there is nothing to
// fail closed on. Writes that grant go through Store.UpdateAudited instead.
func (g *addonHostGrants) auditAppend(e hostgrants.AuditEntry) {
if err := g.audit.Append(e); err != nil {
xlog.Warn("Could not write an addon host grants audit line", "event", e.Event, "error", err)
}
}
// Stage records pending grants for one lifecycle operation. They are part of
// the entry of the add-on but take effect only when the operation completed.
// An entry that exists keeps working: the new operations carry the ID of the
// operation, the old ones their own.
func (g *addonHostGrants) Stage(ctx context.Context, s addons.GrantStage) error {
if g.locked {
g.auditAppend(hostgrants.AuditEntry{Event: hostgrants.AuditRefused, AddonID: s.AddonID, OperationID: s.OperationID, Reason: "locked_by_deployment", By: s.Actor.UserID})
return errHostGrantsLocked
}
source := hostgrants.SourceInstall
if s.Action == "upgrade" {
source = hostgrants.SourceUpgrade
}
var blanketMode string
var blanketRev uint64
if s.Blanket != "" {
m := blanketName.FindStringSubmatch(s.Blanket)
if m == nil {
return fmt.Errorf("record host grants: unknown blanket %q", s.Blanket)
}
blanketMode = m[1]
blanketRev, _ = strconv.ParseUint(m[2], 10, 64)
// Revision 0 means the switch was never set in the document: the
// deployment fixed the mode. Those grants stay the install's own, since
// a blanket grant must name a revision of the document.
if blanketRev != 0 {
source = hostgrants.SourceBlanket
}
}
for range 3 {
snap, err := g.store.Snapshot(ctx)
if err != nil {
return err
}
prev := snap.Grants[s.AddonID]
if prev.Identity == s.Identity && stagedBy(prev, s.OperationID) {
return nil
}
if s.ExpectedRevision != 0 && s.ExpectedRevision != snap.Revision {
g.auditAppend(hostgrants.AuditEntry{Event: hostgrants.AuditRefused, AddonID: s.AddonID, OperationID: s.OperationID, Reason: "revision_conflict", By: s.Actor.UserID})
return addons.ErrGrantsConflict
}
patch := hostgrants.Patch{}
if s.Blanket != "" {
// The mode may come from the deployment, which is never written to the
// document; only the revision the grants were planned against counts.
if blanketRev != snap.Blanket.Revision {
return addons.ErrGrantsConflict
}
}
entry := hostgrants.AddonGrants{Identity: s.Identity, Generation: s.Generation, OperationID: s.OperationID}
have := map[string]bool{}
fresh := true
if len(prev.Ops) > 0 && prev.Identity == s.Identity {
entry.Ops, entry.Generation, entry.OperationID = slices.Clone(prev.Ops), s.Generation, prev.OperationID
for _, op := range prev.Ops {
have[op.Name] = true
}
fresh = false
}
var added []string
for _, name := range s.Ops {
if have[name] {
continue
}
op := hostgrants.OpGrant{Name: name, Source: source, At: g.now(), By: s.Actor.UserID, Version: s.Version, BlanketRevision: blanketRev}
if !fresh {
op.OperationID = s.OperationID
}
entry.Ops = append(entry.Ops, op)
added = append(added, name)
}
if len(added) == 0 {
return nil
}
patch.Addons = map[string]*hostgrants.AddonGrants{s.AddonID: &entry}
ev := hostgrants.AuditGrant
tag := ""
if s.Blanket != "" {
ev, tag = hostgrants.AuditBlanketGrant, blanketMode+"@rev"+strconv.FormatUint(blanketRev, 10)
}
_, err = g.store.UpdateAudited(ctx, patch, snap.Revision, "stage-"+s.OperationID+"-r"+strconv.FormatUint(snap.Revision, 10), func(n hostgrants.Snapshot) error {
return g.audit.Append(hostgrants.AuditEntry{Event: ev, AddonID: s.AddonID, Ops: added, Source: string(source), Version: s.Version, Digest: s.Digest,
OperationID: s.OperationID, RevisionBefore: snap.Revision, RevisionAfter: n.Revision, By: s.Actor.UserID, Credential: s.Actor.Mode, Blanket: tag})
})
if errors.Is(err, hostgrants.ErrConflict) && s.ExpectedRevision == 0 {
continue
}
if errors.Is(err, hostgrants.ErrConflict) {
return addons.ErrGrantsConflict
}
if err != nil {
return err
}
return nil
}
return addons.ErrGrantsConflict
}
// stagedBy reports whether the entry already holds the operations of the
// lifecycle operation, so a continued operation does not stage twice.
func stagedBy(e hostgrants.AddonGrants, opID string) bool {
for _, op := range e.Ops {
if hostgrants.EffectiveOperationID(e, op) == opID {
return true
}
}
return false
}
// Abort removes what an operation that did not complete staged, and nothing
// else: the operations the add-on held before keep working.
func (g *addonHostGrants) Abort(ctx context.Context, operationID string) error {
for range 3 {
snap, err := g.store.Snapshot(ctx)
if err != nil {
return err
}
patch := hostgrants.Patch{Addons: map[string]*hostgrants.AddonGrants{}}
var audits []hostgrants.AuditEntry
for id, e := range snap.Grants {
var kept []hostgrants.OpGrant
var dropped []string
for _, op := range e.Ops {
if hostgrants.EffectiveOperationID(e, op) == operationID {
dropped = append(dropped, op.Name)
} else {
kept = append(kept, op)
}
}
if len(dropped) == 0 {
continue
}
if len(kept) == 0 {
patch.Addons[id] = nil
} else {
e.Ops = kept
patch.Addons[id] = &e
}
audits = append(audits, hostgrants.AuditEntry{Event: hostgrants.AuditRefused, AddonID: id, Ops: dropped, OperationID: operationID, Reason: "operation_not_completed", RevisionBefore: snap.Revision})
}
if len(patch.Addons) == 0 {
return nil
}
// Removing pending grants is not a grant: a failed audit line must not
// keep a half-staged entry, so this write is not gated on the audit.
res, err := g.store.Update(ctx, patch, snap.Revision, "abort-"+operationID+"-r"+strconv.FormatUint(snap.Revision, 10))
if errors.Is(err, hostgrants.ErrConflict) {
continue
}
if err != nil {
return err
}
for _, e := range audits {
e.RevisionAfter = res.Snapshot.Revision
g.auditAppend(e)
}
return nil
}
return hostgrants.ErrConflict
}
// Remove deletes the grants of an uninstalled add-on and, when asked, keeps a
// hint that pre-ticks a later install. A hint grants nothing.
func (g *addonHostGrants) Remove(ctx context.Context, addonID string, keepHint bool) error {
for range 3 {
snap, err := g.store.Snapshot(ctx)
if err != nil {
return err
}
entry, had := snap.Grants[addonID]
_, hinted := snap.Hints[addonID]
if !had && !(hinted && !keepHint) {
return nil
}
patch := hostgrants.Patch{Addons: map[string]*hostgrants.AddonGrants{addonID: nil}}
var names []string
for _, op := range entry.Ops {
names = append(names, op.Name)
}
if keepHint && len(names) > 0 {
patch.Hints = map[string][]string{addonID: names}
} else if hinted {
patch.Hints = map[string][]string{addonID: nil}
}
res, err := g.store.Update(ctx, patch, snap.Revision, "remove-"+addonID+"-r"+strconv.FormatUint(snap.Revision, 10))
if errors.Is(err, hostgrants.ErrConflict) {
continue
}
if err != nil {
return err
}
if had {
g.auditAppend(hostgrants.AuditEntry{Event: hostgrants.AuditRevoke, AddonID: addonID, Ops: names, Reason: "uninstalled", RevisionBefore: snap.Revision, RevisionAfter: res.Snapshot.Revision})
}
return nil
}
return hostgrants.ErrConflict
}
// Reconcile drops the entries that no longer match the journal.
func (g *addonHostGrants) Reconcile(ctx context.Context, installed map[string]string, completed func(string) bool) error {
removed, err := g.store.Reconcile(ctx, hostgrants.ReconcileInput{
Installed: func(id string) (string, bool) { identity, ok := installed[id]; return identity, ok },
OperationCompleted: completed,
})
for _, r := range removed {
g.auditAppend(hostgrants.AuditEntry{Event: hostgrants.AuditReconcileRemove, AddonID: r.AddonID, Ops: r.Ops, Reason: r.Reason})
}
return err
}
// Blanket says what the blanket switch grants to a package, when it is on and
// the package is in its scope.
func (g *addonHostGrants) Blanket(ctx context.Context, q addons.BlanketQuery) ([]string, string) {
if g.locked || g.store.Usable() != nil {
return nil, ""
}
snap := g.store.View()
id := q.Receipt.Manifest.ID
res := g.floor.EvaluateBlanket(snap, g.context(id), g.override, hostgrants.BlanketInput{
AddonID: id, Official: addons.OfficialReceipt(q.Receipt), Required: q.Required, Optional: q.Optional,
})
if !res.Applies || len(res.Plan.Grant) == 0 {
return nil, ""
}
mode, rev := snap.Blanket.Mode, res.Revision
// A mode the deployment fixes is applied here, at read time, and is never
// copied into the document: removing the flag must hand the switch back to
// what a person set.
if g.override != "" {
mode = g.override
}
return res.Plan.Grant, fmt.Sprintf("blanket:%s@rev%d", mode, rev)
}
// --- gateway.HostAccessManager ---
type hostAccessManager struct{ m *addons.Manager }
func (h hostAccessManager) HostAccess(ctx context.Context) ([]gateway.InstalledAccess, error) {
list, err := h.m.HostAccessInstalled(ctx)
if err != nil {
return nil, err
}
out := make([]gateway.InstalledAccess, 0, len(list))
for _, in := range list {
a := gateway.InstalledAccess{AddonID: in.AddonID, Title: in.Title, Version: in.Version, State: in.State, Identity: in.Identity,
Generation: in.Generation, OperationID: in.OperationID, Official: in.Official, Required: in.Required, Optional: in.Optional,
SourceID: in.SourceID, NewOptional: in.NewOptional}
if in.HeldVersion != "" {
a.PendingUpgrade = &gateway.PendingUpgrade{Version: in.HeldVersion, Delta: in.HeldDelta}
}
out = append(out, a)
}
return out, nil
}
func (h hostAccessManager) PreviewHostAccess(ctx context.Context, sourceID, addonID, version string) (gateway.ReleaseAccess, error) {
r, err := h.m.PreviewRelease(ctx, sourceID, addonID, version)
if err != nil {
return gateway.ReleaseAccess{}, err
}
return gateway.ReleaseAccess{AddonID: r.AddonID, Title: r.Title, Version: r.Version, Digest: r.Digest, Official: r.Official,
Required: r.Required, Optional: r.Optional, Installed: r.Installed}, nil
}
func (h hostAccessManager) SubmitWithApproval(ctx context.Context, actor contract.Actor, cmd addons.LifecycleCommand, ap gateway.HostApproval) (addons.OperationStatus, error) {
cmd.Approval = &addons.HostApproval{Digest: ap.Digest, Ops: ap.Ops, Revision: ap.Revision, Nonce: ap.Nonce}
st, err := h.m.Submit(ctx, actor, cmd)
// The owner service types its refusals by these errors.
var stale *addons.ApprovalStaleError
var refusal *addons.HostRequirementsRefusal
switch {
case errors.As(err, &stale):
return st, gateway.ErrApprovalStale
case errors.As(err, &refusal) && refusal.Blocked():
return st, gateway.ErrBlockedByPolicy
case errors.As(err, &refusal):
return st, gateway.ErrApprovalRequired
}
return st, err
}
// wireHostGrantsOwner gives the gateway of the owner the host access routes.
// A frontend has no grants and forwards.
func (a *Application) wireHostGrantsOwner(g *gateway.Gateway) error {
hg := a.addonGrants
if hg == nil || hg.store == nil || a.addonManager == nil {
return nil
}
owner, err := gateway.NewHostGrantsOwner(gateway.HostGrantsConfig{
Store: hg.store, Floor: hg.floor, Audit: hg.audit, Manager: hostAccessManager{a.addonManager},
Supported: hostops.SupportedOperations(),
Describe: func(op string) (gateway.HostOpDescription, bool) {
title, text, risk, ok := hostops.DescribeOperation(op)
return gateway.HostOpDescription{Title: title, Text: text, Risk: risk}, ok
},
BlanketOverride: hg.override, Locked: hg.locked, LockedReason: hg.lockedReason,
Completed: a.addonManager.OperationCompleted,
Admin: g.HostSettingsAdmin,
AfterChange: a.AddonHostGrantsAfterChange,
})
if err != nil {
return err
}
// The same service takes the zip files of an install from a file.
g.HostGrantsOwner = owner.WithFiles(a.addonManager)
return nil
}