feat(addons): add schemaVersion 2 host policy with a deny list

A policy file may now carry a deny object. Deny rules name an add-on ID
or "*" and list prefixes, exact operation names or the single token "*".
Schema 1 files parse as before; a deny key in a schema 1 file, or a
version above 2, is an error that names the problem.

The selected file is now always opened, even when a flat
--addon-host-ops list replaces its grants, because a deny must not
vanish when someone adds a flag. An invalid selected file still stops
startup.

Add --addon-host-deny (LOCALAI_ADDON_HOST_DENY), unioned with the file,
and the --addon-host-blanket and --addon-host-ui-grants flags with their
config options.

Assisted-by: Claude:claude-sonnet-5-5 go
This commit is contained in:
Ettore Di Giacinto committed 2026-11-03 21:22:56 +00:00
1 parent 3607c08c17
commit 082fd7ee7d
7 files changed
+442 -104

No files matched your search

+10
View File
@@ -43,6 +43,9 @@ type RunCMD struct {
P2PEnableLimits string `name:"p2p-enable-limits" env:"LOCALAI_P2P_ENABLE_LIMITS" help:"Set to true to enable P2P resource limits" group:"p2p"`
AddonHostPolicy string `name:"addon-host-policy" env:"LOCALAI_ADDON_HOST_POLICY" help:"Absolute path to administrator host operation policy JSON, read once at startup. Empty disables environment policy" group:"addons"`
AddonHostOps string `name:"addon-host-ops" env:"LOCALAI_ADDON_HOST_OPS" help:"Add-ons that may call host operations, as a comma list of addon-id=operation-prefix. An add-on that is not listed gets no host operation channel. Empty gives none" group:"addons"`
AddonHostDeny string `name:"addon-host-deny" env:"LOCALAI_ADDON_HOST_DENY" help:"Host operations no grant may enable, as a comma list of addon-id=operation (a prefix such as egress., an exact name or *). Use *=prefix for every add-on. Deny always wins over grants and is unioned with the deny rules of the policy file" group:"addons"`
AddonHostBlanket string `name:"addon-host-blanket" env:"LOCALAI_ADDON_HOST_BLANKET" help:"Blanket approval of declared host operations at install and upgrade: off, official or any. When set, the UI shows it read-only. Unset leaves the choice to the administrator in the UI" group:"addons"`
AddonHostUIGrants bool `name:"addon-host-ui-grants" env:"LOCALAI_ADDON_HOST_UI_GRANTS" default:"true" help:"Let an administrator grant host operations to add-ons in the UI. Turn it off with --addon-host-ui-grants=false to make the policy file and flags the only source of grants" group:"addons"`
AddonsEnabled bool `name:"addons-enabled" env:"LOCALAI_ADDONS_ENABLED" default:"true" help:"Run the add-on owner, which installs and supervises add-ons. Turn it off with --addons-enabled=false" group:"addons"`
AddonsOwnershipMode string `name:"addons-ownership-mode" env:"LOCALAI_ADDONS_OWNERSHIP_MODE" help:"Who may use add-ons: named-user (each account has its own access; needs authentication) or local-owner (every caller shares one owner). Default: local-owner when authentication is off, named-user when it is on" group:"addons"`
AddonsRole string `name:"addons-role" env:"LOCALAI_ADDONS_ROLE" default:"combined" help:"How this process takes part in the add-on owner: combined (this node is its own owner, the default), owner (only the owner, serves the carrier on the network) or frontend (no owner here; every add-on request goes to the owner). owner and frontend need --addons-owner-address and --addons-carrier-dir" group:"addons"`
@@ -329,6 +332,10 @@ func (r *RunCMD) Run(ctx *cliContext.Context) (result error) {
return err
}
logAddonHostOpsResolution(hostOps)
blanket, err := config.ParseAddonHostBlanket(r.AddonHostBlanket)
if err != nil {
return err
}
diagnosticOpts := r.diagnosticsOptions()
if err := diagnosticOpts.Validate(); err != nil {
@@ -384,6 +391,9 @@ func (r *RunCMD) Run(ctx *cliContext.Context) (result error) {
}
opts := []config.AppOption{
config.WithAddonHostOps(hostOps.Grants),
config.WithAddonHostDeny(hostOps.Deny),
config.WithAddonHostBlanket(blanket),
config.WithAddonHostUIGrantsLocked(!r.AddonHostUIGrants),
config.WithProxyAPIKeyEnvLookup(os.Getenv),
config.WithContext(runCtx),
config.WithArtifactDownloadConcurrency(r.ArtifactDownloadConcurrency),
+44 -9
View File
@@ -36,7 +36,7 @@ var _ = Describe("addon host policy CLI precedence", Serial, func() {
return &command.Run
}
BeforeEach(func() {
for _, key := range []string{"LOCALAI_ADDON_HOST_OPS", "LOCALAI_ADDON_HOST_POLICY"} {
for _, key := range []string{"LOCALAI_ADDON_HOST_OPS", "LOCALAI_ADDON_HOST_POLICY", "LOCALAI_ADDON_HOST_DENY"} {
old, ok := os.LookupEnv(key)
DeferCleanup(func() {
if ok {
@@ -57,31 +57,66 @@ var _ = Describe("addon host policy CLI precedence", Serial, func() {
Expect(got.Source).To(Equal("policy:" + p))
Expect(got.Grants).To(Equal(map[string][]string{"custom": {"workspace."}}))
})
It("uses flag presence even when empty and ignores an invalid policy", func() {
writePolicy := func(body string) string {
p := filepath.Join(GinkgoT().TempDir(), "policy.json")
Expect(os.WriteFile(p, []byte(body), 0600)).To(Succeed())
return p
}
It("uses flag presence even when empty, ignores the file grants and still applies its deny", func() {
GinkgoT().Setenv("LOCALAI_ADDON_HOST_OPS", "env=x.")
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", "/missing/policy")
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", writePolicy(`{"schemaVersion":2,"grants":{"file":["z."]},"deny":{"*":["egress."]}}`))
got, err := parse("--addon-host-ops=").resolveAddonHostOps()
Expect(err).NotTo(HaveOccurred())
Expect(got.Grants).To(BeEmpty())
Expect(got.Source).To(Equal("flag"))
Expect(got.PolicyIgnored).To(BeTrue())
Expect(got.GrantsIgnored).To(BeTrue())
Expect(got.Deny).To(Equal(map[string][]string{"*": {"egress."}}))
})
It("uses explicit empty environment instead of policy", func() {
It("uses explicit empty environment instead of file grants and still applies its deny", func() {
GinkgoT().Setenv("LOCALAI_ADDON_HOST_OPS", "")
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", "/missing/policy")
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", writePolicy(`{"schemaVersion":2,"deny":{"a":["*"]}}`))
got, err := parse().resolveAddonHostOps()
Expect(err).NotTo(HaveOccurred())
Expect(got.Grants).To(BeEmpty())
Expect(got.Source).To(Equal("environment"))
Expect(got.PolicyIgnored).To(BeTrue())
Expect(got.GrantsIgnored).To(BeTrue())
Expect(got.Deny).To(Equal(map[string][]string{"a": {"*"}}))
})
It("does not union a nonempty flag with environment or policy", func() {
GinkgoT().Setenv("LOCALAI_ADDON_HOST_OPS", "env=x.")
It("stops startup on an invalid selected file even when a flat list replaces its grants", func() {
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", "/missing/policy")
_, err := parse("--addon-host-ops=flag=y.").resolveAddonHostOps()
Expect(err).To(HaveOccurred())
})
It("does not union a nonempty flag with the file grants", func() {
GinkgoT().Setenv("LOCALAI_ADDON_HOST_OPS", "env=x.")
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", writePolicy(`{"schemaVersion":1,"grants":{"file":["z."]}}`))
got, err := parse("--addon-host-ops=flag=y.").resolveAddonHostOps()
Expect(err).NotTo(HaveOccurred())
Expect(got.Grants).To(Equal(map[string][]string{"flag": {"y."}}))
})
It("unions the deny flag with the file deny", func() {
got, err := parse("--addon-host-policy="+writePolicy(`{"schemaVersion":2,"deny":{"web":["workspace.admin"]}}`), "--addon-host-deny=web=egress.").resolveAddonHostOps()
Expect(err).NotTo(HaveOccurred())
Expect(got.Deny).To(Equal(map[string][]string{"web": {"workspace.admin", "egress."}}))
})
It("reads the deny environment variable", func() {
GinkgoT().Setenv("LOCALAI_ADDON_HOST_DENY", "*=connector.")
got, err := parse("--addon-host-policy=" + writePolicy(`{"schemaVersion":2,"deny":{"web":["workspace.admin"]}}`)).resolveAddonHostOps()
Expect(err).NotTo(HaveOccurred())
Expect(got.Deny).To(Equal(map[string][]string{"web": {"workspace.admin"}, "*": {"connector."}}))
})
It("rejects an invalid deny flag and blanket mode", func() {
_, err := parse("--addon-host-deny=nonsense").resolveAddonHostOps()
Expect(err).To(HaveOccurred())
_, err = config.ParseAddonHostBlanket("sometimes")
Expect(err).To(HaveOccurred())
})
It("leaves UI grants on and the blanket unset by default", func() {
r := parse()
Expect(r.AddonHostUIGrants).To(BeTrue())
Expect(r.AddonHostBlanket).To(BeEmpty())
Expect(parse("--addon-host-ui-grants=false").AddonHostUIGrants).To(BeFalse())
})
It("lets an empty policy flag disable environment policy without restoring defaults", func() {
GinkgoT().Setenv("LOCALAI_ADDON_HOST_POLICY", "/missing/policy")
got, err := parse("--addon-host-policy=").resolveAddonHostOps()
+16 -6
View File
@@ -41,7 +41,13 @@ func (r *RunCMD) resolveAddonHostOps() (config.AddonHostOpsResolution, error) {
} else if value, present := os.LookupEnv("LOCALAI_ADDON_HOST_POLICY"); present {
policy = &value
}
resolved, err := config.ResolveAddonHostOps(flag, environment, policy)
var deny *string
if r.AddonHostDeny != "" {
deny = &r.AddonHostDeny
}
// Kong has already merged the deny environment variable into the field,
// so there is no separate environment value to union.
resolved, err := config.ResolveAddonHostOps(flag, environment, policy, deny, nil)
if err == nil && resolved.Source == "policy" {
resolved.Source = "policy:" + *policy
}
@@ -50,15 +56,19 @@ func (r *RunCMD) resolveAddonHostOps() (config.AddonHostOpsResolution, error) {
func logAddonHostOpsResolution(resolved config.AddonHostOpsResolution) {
if resolved.Source == "empty" {
xlog.Warn("Addons receive no host operations: select --addon-host-policy or explicitly configure --addon-host-ops")
xlog.Warn("Addons receive no host operations from the policy file or flags: an administrator can grant them in the UI at install, or select --addon-host-policy or --addon-host-ops")
}
prefixes := 0
for _, grants := range resolved.Grants {
prefixes += len(grants)
}
xlog.Info("Addon host operation policy resolved", "source", resolved.Source, "addons", len(resolved.Grants), "prefixes", prefixes)
if resolved.PolicyIgnored {
xlog.Warn("Addon host policy ignored: explicit host operations override takes precedence")
denied := 0
for _, entries := range resolved.Deny {
denied += len(entries)
}
xlog.Info("Addon manifests and libraries cannot grant host operations; policy changes require restart")
xlog.Info("Addon host operation policy resolved", "source", resolved.Source, "addons", len(resolved.Grants), "prefixes", prefixes, "denyRules", denied)
if resolved.GrantsIgnored {
xlog.Warn("Addon host policy grants ignored, deny applied: an explicit host operations list replaces the file grants")
}
xlog.Info("Addon manifests and libraries cannot grant host operations; file and flag policy changes require restart")
}