Skip to content

feat(addons): installable add-ons foundation and the Tables add-on - #12702

Open
mudler-agent wants to merge 264 commits into
masterfrom
feat/addons-tables
Open

mudler-agent wants to merge 264 commits into
masterfrom
feat/addons-tables

Conversation

@mudler-agent

Copy link
Copy Markdown
Collaborator

Status: ready for review. The whole plan is implemented and tested: the
foundation, Tables, recovery, packaging, update detection and an end-to-end
Tables acceptance suite. A few residual items are listed under "Open items".

What

An add-on is an installable extension that runs as a managed process next to
LocalAI, and can bring tools for models, services, and UI. This PR adds:

  • The add-on foundation.
    • Signed/verified package staging, immutable generations, a durable lifecycle
      journal and a single designated owner that supervises add-on processes.
    • Authenticated loopback transport between the host and the add-on process.
    • Owner forwarding: every frontend replica relays JSON, progress, files and UI
      assets through the owner over mutual TLS. If the owner is down, requests
      fail closed; nothing runs locally.
    • Per-user, private-by-default resources with Viewer / Editor / Owner roles,
      host-owned admission and revocation that waits for admitted work.
    • Stored proposals with host-owned Confirm/Cancel for every chat mutation.
      Models, agents and tool calls can propose but never approve.
    • Private, expiring file results (artifacts) with ACL checks at completion and
      download.
    • Catalog sources (public and private libraries by credential reference).
    • Backup, upgrade with migration, restore and retained-data purge.
    • First-party OCI packaging and a trusted-CI release workflow with branch
      channels, development (master only) and latest (published release).
    • Channel-aware update detection and an explicit, pinned administrator
      Upgrade.
  • The UI.
    • An Apps page for all signed-in users, with Start/Stop for administrators,
      and an administrator view to browse, install, activate and manage add-ons.
    • A versioned host SDK, immutable asset serving, and a closed registry of host
      slots (chat composer actions, per-message actions, navigation, settings
      sections), plus approval-card and tool-result renderers.
  • Tables, the first add-on: SQLite-backed databases per user, typed schema
    operations, structured queries and row changes (no raw SQL), whole-database
    sharing, a native UI, a "Select database" chat action, tools for models, and
    private CSV export.

Why

LocalAI users want to extend it with tools and small apps without rebuilding
the host or giving every extension access to everything. The design keeps
authority in the host (identity, roles, approval, quotas) and keeps the add-on
code narrow.

How to try it

  1. Build as usual (make build; the React UI is built by the normal UI build).
  2. Build the Tables package: go build ./addons/tables/cmd/tables and build the
    UI package in addons/tables/ui (node build.mjs).
  3. Enable the add-ons feature for an administrator account, open Add-ons in
    the web UI, add a library that lists the package, install Tables and
    activate it.
  4. Open Apps, open Tables, create a database. In chat, enable add-on tools
    with /addons, pick a database with the Select database action and ask
    the model to read or change rows. Changes appear as a proposal card that you
    confirm in the UI.

Documentation: docs/content/features/addons.md,
docs/content/features/tables.md and docs/content/development/addon-testing.md.

Tests

Every suite runs offline through sh scripts/addons/verify.sh <suite>, which
enforces an exact case inventory and zero skips. Pass counts at the last full
run (each includes the readiness checks the runner adds):

Suite Passed
prerequisites 39
contracts / catalog / packages 21 / 24 / 22
lifecycle / runtime 44 / 32
gateway / policy / proposals / artifacts 126 / 119 / 188 / 163
apps 147
gallery-ui / esm-ui / host-slots 49 / 59 / 35
foundation-acceptance 44
tables-databases / -schema / -rows 93 each
tables-tools / tables-admin / tables-ui / tables-csv 63 / 40 / 61 / 40
recovery 77
packaging 113
updates 73
tables-acceptance 41

sh scripts/addons/foundation_gate.sh and sh scripts/addons/tables_gate.sh
run the foundation and Tables suites in order, reap leaked processes and print
an inventory. Go race tests for core/services/addons/...,
core/http/endpoints/addons and pkg/addons/... pass, as do go vet and the
existing application, config, auth and endpoint tests.

tables-acceptance runs the real packaged Tables on an owner and two
forwarding frontends over mutual TLS with real sessions, API keys and SQLite,
and writes an evidence manifest (cases, platform, artifact digests). Only
linux/amd64 is covered natively.

The release workflow cannot be executed offline: registry login, signing and
verification are covered by workflow-policy assertions and local OCI layouts,
not by a live run.

Known limits

  • Add-on UI code is native, same-origin code. It is trusted at install time
    and is not sandboxed. Controls are install-time trust, server-side
    authorization on every call and a host-owned approval frame.
  • Local-owner mode (no accounts) shares authority between everyone who can
    reach the port, including confirmation of proposals. It is explicit opt-in and
    warns at startup.
  • Admin lifecycle actions (install, enable, upgrade, uninstall) are not exposed
    as model tools; this is a deliberate deferral and documented.
  • Chat database creation, rename, schema and row changes go through proposals;
    deletion and sharing are UI-only.
  • Release platforms: the platform matrix ships with every platform disabled
    (enabled: false) until each has a native runtime check; the publishing
    workflow fails closed without a registry namespace and an enabled platform.
  • The signature policy of a library pins one exact signer identity, which
    includes the release ref; supporting an identity pattern needs a catalog
    change and is documented.
  • Data revisions are per database (conservative): any committed change makes a
    pending row proposal stale.
  • An upgrade request blocks until the owner finishes; the page shows a spinner
    and the recovery panel shows the phases.
  • The add-on admits four concurrent calls; beyond that a call is refused as
    unavailable (an unknown outcome for mutations, recovered by repeating the
    same operation ID).

Open items

  • A review-free window: a few late fix commits (packaging fix, some Tables UI
    fixes, the acceptance burst change) were verified by their tests and the full
    gate but did not get a second independent review pass.
  • The core/http/endpoints/localai websocket-logs spec is an existing flaky
    test that fails occasionally under load; it is unrelated to this change.
  • A proposal confirmed against an old add-on generation answers 403 but stays
    listed as pending until it expires.
  • The first CI run of the release workflow needs a registry namespace, an
    enabled platform and an environment with signing permission; none are
    configured here.

🤖 Generated with Claude Code

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gosec found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Comment thread pkg/addons/model/proposalflow/cmd/viewer/main.go Fixed
@mudler
mudler marked this pull request as ready for review October 16, 2026 16:48
)

// HostCredentialHeader carries the host credential of the incarnation.
const HostCredentialHeader = "X-Addon-Host-Credential"
if path == "" {
return s, nil
}
b, err := os.ReadFile(path)
mudler added 24 commits October 20, 2026 06:53
Forward typed addon operations from any frontend to the designated owner
over pinned mutual TLS with signed, body-bound, expiring envelopes and
replay protection. The owner rechecks current credential and account
status and fails closed when identity or resource policy is unavailable.

Relay JSON, coarse lifecycle progress, attachment bytes and immutable UI
assets through the receiving frontend with bounded streams and
cancellation. An uncertain mutation dispatch reports the original
operation and is never retried as a new one.

Add actor-bound read-only lifecycle status lookup, feature and admin
gated routes with CSRF checks, API discovery entries and docs.

Verify gateway, runtime, lifecycle, packages, catalog, contracts and
prerequisites. Run auth application and config tests and race tests.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The owner handler turned every Relay error into an aborted connection,
so a denial made before the first byte (invalid kind, missing resource
policy, refused or revoked resource) reached the receiving frontend as
"owner unavailable" instead of forbidden. The same decision therefore
produced different errors depending on the frontend that served it.

Track whether the owner committed any response byte. Before the first
byte, return a typed status through one shared mapping (403 for a
denial, 503 for unavailability) that the frontend already decodes back
into ErrForbidden or ErrUnavailable. After the first byte the status can
no longer change, so the stream is still aborted and the frontend sees a
truncated stream, never a clean end.

The receiving frontend now also bounds each relay kind by its own limit
instead of the file limit: progress and status use ProgressBytes, asset
uses AssetBytes, json is capped at 2 MiB and file keeps Bytes. An unknown
kind is denied before the owner is contacted.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Add behavioural coverage for the owner forwarding gateway and replace
assertions that could not fail against an always-forbidden or empty
implementation.

- Owner denials before the first byte reach every frontend as
  forbidden; a revocation after bytes were written still aborts, and an
  owner that cannot serve reports unavailable.
- Per-kind frontend budgets: file, progress, asset and an unknown kind.
- A CA signed peer that is not pinned is refused by the owner handler,
  and a CA signed owner that is not the pinned owner is refused by the
  frontend before any request is sent. A pinned control proves the pin,
  not the CA, decides the outcome.
- Gateway.asset against a real installed package: generation, operation
  ownership, asset size limit, media types, undeclared files and
  traversal.
- The registered /api/addons routes through real authentication:
  forged forwarding headers, feature gate, administration gate and
  same-origin requirement for cookie mutations.
- Replace the construction, admission and typed-ingress tests with
  assertions that fail on an always-forbidden implementation: complete
  pin and address validation, admission held while the output is
  described, the original operation ID of an unknown mutation outcome,
  and unknown lifecycle fields leaving the journal untouched next to an
  accepted request in the same setup.
- Keep the suite inventory in sync.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The dispatch and relay handlers carried only @tags and @router, so the
generated API reference had no addon operations, parameters or
responses. Add full annotations (summary, description, content types,
request body, success and failure responses) and regenerate the
Swagger artifacts with the repository's generator settings.

Raw JSON fields of the typed request, result and preview use the
existing swaggertype:"object" convention so the schemas resolve.

The HTTP endpoints now derive 403 versus 503 from the same
gateway.StatusFor mapping the owner uses, so a local error and a
forwarded error of the same kind produce the same status.

The two routes are deliberately not wired into the LocalAI Assistant
tool catalog: lifecycle mutation tools are deferred until the brokered
tool integration defines confirmation semantics, and the route map test
only requires an entry for each registered tool. Record that decision
in the addon documentation instead of weakening a test.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Gateway.Relay arms a goroutine that expires the response write deadline
when the relay context ends. The deferred cancel fires right after the
relay returns, so the goroutine could win the race against the done
signal and expire the deadline just before the caller wrote its final
bytes. A typed denial status written by the owner handler, or the final
flush of a buffered response, was then dropped and the receiving
frontend reported the owner as unavailable. The failure showed up under
the race detector.

Only expire the deadline while the relay is still running. A regression
test arms 200 relays and checks that no deadline is ever moved to the
past after Relay returns.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Carrier.Relay returned success when reading the owner response ended
with a clean end of stream, even if the relay context had been canceled
or had expired at the same moment. A disconnected or timed out relay
could therefore look like a finished one. About one run in ten of the
interrupted progress cancellation spec failed under load for this
reason.

Return the context error when the stream ends while the context is no
longer live. The spec now passes 120 of 120 runs under parallel load.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The owner mapped every failure other than a few unavailability errors to
403, so a runtime that was not installed, not serving, in recovery or
busy, an output that ended before its first byte, and any unclassified
error were reported to users as a policy denial.

Only explicit authorization, policy and request rejections are 403 now.
Every other error, including an unclassified one, is 503, so an unknown
failure is never reported as a decision or as success. The owner marks a
refusal that happened before anything ran, and the receiving frontend
decodes it to plain unavailability instead of an unknown mutation
outcome, so both frontends report the same error for the same cause.

The receiving frontend also copied the owner's attachment headers before
the first body read. A failed first read then returned an error response
that still carried Content-Disposition. The headers are now copied only
when the first byte is committed or the stream ends cleanly.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A previous change let the owner mark a typed 503 with a refusal header so
a frontend reported a refused mutation as plain unavailability. The
owner chose the marker by error identity, not by whether it had already
run anything. The lifecycle manager can return a runtime or recovery
error after it journaled and applied the operation, so the marker made
an uncertain mutation look as if it had not run. A caller could then
submit it again under a new identifier.

Remove the marker. A mutation that gets a 503 or a transport failure
after dispatch stays an unknown outcome with its original operation
identifier, and the frontend does not dispatch it again. The 403 and
503 taxonomy, the explicit denial allowlist and the fail-closed default
stay. An error that wraps both a denial and an unavailable cause is
503, and a test pins that order.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A mutation that failed as unavailable on the owner's own frontend was
returned as a plain 503 with no operation identifier. The owner can fail
after it recorded or applied the mutation, so a caller could submit a
fresh mutation for work that already ran.

Classify the local dispatch result with the same status taxonomy the
forwarding carrier uses, in one shared function. A mutation that is
unavailable now returns an unknown outcome with the original operation
identifier from every frontend. Reads, status polls and denials keep
their status.

Add endpoint tests that drive the dispatch route and check the status,
error type and operation identifier for an unknown outcome, an
unavailable read, a denial and the owner-local mutation.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The addon endpoints annotated their failures with the generic OpenAI
error schema. That schema has no operationId, so the outcome_unknown
body was not described, and the type of code did not match.

Define the error body in the addons endpoint package, emit every failure
through it, and reference it from the annotations. Regenerate the
Swagger files from the annotations. A test now checks that every field a
503 response emits is documented. Describe the body in the addons
guide.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Classify only the read and operation-status actions as non-mutating, so
an action added later reports an unknown outcome with its original
operation identifier until it is explicitly classified. Relay requests
remain read-only by kind.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Add an owner-local policy store that decides who may touch an addon
resource, separate from replaceable packages. Resources are opaque
host IDs bound to the trusted addon identity and an immutable owner.

Roles are Viewer, Editor and Owner on whole resources. Installation
administration confers no data access, named-user mode rejects
anonymous and legacy principals, and explicit local-owner mode turns
sharing off. Switching the mode of populated data needs a stopped,
backed-up migration; shared data cannot return to local-owner mode.

Admission rechecks the account and credential, serializes writes per
resource and shares one lock with grant changes. Revocation is
durable first, invalidates pending proposals through a seam, cancels
the user's admitted work and returns only after it ended or the bound
expired. New resources are registered pending and reconciled after a
crash, so an orphan never becomes reachable.

The owner installs the policy as the gateway admission and resolver;
frontends and hosts without it keep denying.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The gateway mapped every failed host account check to a denial, so a
broken account store looked like a revoked user. A check that wraps
the unavailable cause now returns 503 from dispatch, relay and the
forwarded-actor verifier; any other refusal stays 403.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A downgrade that failed to drain left the lowered grant durable, so
the identical retry saw no change and was acknowledged while the old
write still ran. A drain mark is now stored with the change and
cleared only after admitted work ended and proposals were
invalidated; any retry waits for it, also across a restart.

Admission checks the revocation state under the gate right before it
runs the work, so a host check that ignores cancellation cannot start
work for a revoked user. Account checks that report an outage give
503 instead of 403.

Registrations are capped per user (pending and total) and in the
store with finite defaults. The store rejects unknown roles, grants
to the owner and trailing bytes, removes stale temporary files, and
rolls memory forward and refuses writes when the directory sync fails
after the rename.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Add AddonConfig.OwnershipMode. The default stays named-user and
local-owner is never inferred. In local-owner mode every caller of
the addon routes acts as one reserved synthetic owner, sharing is
denied and the owner logs a startup warning that unauthenticated
callers share one authority. Named-user mode rejects that principal.

Account lookups now separate "not found or disabled" (403) from a
store failure (503).

Creation authority requires the addon to be installed, enabled and
not changing on the owner, and the owner reconciles pending
registrations at startup. Task 13 supplies the real resource check;
the default drops pending registrations and keeps committed ones.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Cover the owed-drain mark, the registration limits, store validation,
the creation authority and the explicit local-owner mode, including
its behaviour without an account database. Add the new cases to the
suite inventory.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The policy and the gateway mapped any error from the host account
check or the creation authority that was not explicitly unavailable to
a denial. An unexpected failure was then reported as a refusal of a
valid user (403) instead of failing closed as unavailable (503).

Only an error that wraps the shared forbidden sentinel is a denial now.
Everything else, including a cancelled check, is unavailable. One
helper, gateway.ClassifyCheck, serves the policy, the gateway dispatch
and relay paths and the forwarding verifier. The auth layer's definite
refusals now wrap the sentinel so they keep answering 403.

Tests that used a plain error to stand for a disabled account now wrap
the forbidden sentinel, since a plain error no longer means a denial.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
While a drain was owed, every retry of the same grant change stopped
and waited for all admitted work of that user. Work admitted after the
grant became durable again was cancelled too, because the mark did not
say which authority it was owed for.

Each resource now keeps an authority epoch that a lowering advances,
and every admission records the epoch it was admitted under. The owed
mark stores the cutoff of its lowering, separately for writes and
reads. A retry invalidates proposals and stops and waits only for work
older than the cutoff. A new lowering starts a new epoch and covers
newer work. The epoch and the cutoff are persisted with the grant, so a
restart keeps both.

A call also clears only the mark it drained, so a lowering that ran
while the call waited keeps its own owed drain.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
…covery

Every data or schema mutation from chat, an agent or an API client is now
a stored proposal. The owner previews it on the addon, stores the
canonical arguments, generation, revisions, exact effect count, a
bounded sample and an expiry, and applies it only on an explicit
confirmation of that stored operation.

- Broker (policy): propose, confirm, cancel and reconcile with the
  pending, cancelled, expired, executing, applied, rejected and
  outcome-unknown states. Each transition is written atomically and
  fsynced; executing is durable before the addon sees the call. The
  broker is the policy's proposal invalidator, so a revocation cancels
  pending proposals before it is acknowledged.
- A lost or crashed apply is outcome-unknown with the original
  operation ID and is resolved by receipt lookup, never by a new apply.
  An unresolved outcome blocks other writes on the resource.
- Gateway: proposal calls travel as their own carrier call, so a typed
  dispatch can never confirm, and other frontends reach the owner's
  state through the signed forwarding path.
- HTTP: POST /api/addons/proposals, GET /api/addons/proposals/{id} and
  POST .../confirm, .../cancel and .../reconcile. Confirm accepts only
  the proposer's browser session from a same-origin fetch that names the
  proposal and its digest, and refuses API keys and tool, agent or MCP
  markers. No MCP tool maps to it.
- Resource IDs now start with a letter. The envelope contract refuses a
  bare hex value that starts with a digit, which would have made most
  new resources unusable at preview time.

The suites inventory, Swagger, the instruction registry, the route
feature registry and the addons guide are updated with the code.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The digest did not cover the proposing user, and the owner only compared
the digest a caller named with the stored one. A stored record whose
arguments, actor, resource, generation or operation ID had been changed
would still run under the digest it was created with.

The digest now includes the proposing user and mode. Confirmation and
the execution step recompute it from the stored record and refuse a
mismatch, with or without a digest from the caller. Tests cover each
bound field in the digest and a record edited on disk for each field.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
… store

A pending proposal past its expiry was never moved to expired or
dropped. Expiry ran only when the proposal was read or confirmed, the
per-user count skipped expired entries, and nothing collected settled
records except when the store was already full. One user making a
proposal every few minutes could fill the 1024 records and deny every
user for good, across restarts.

- Expiry is now recorded by the clock, on each insertion, on start and
  on recovery, durably and without any read.
- Settled proposals are dropped after a retention window (24 hours by
  default, configurable, never disabled).
- The per-user bound counts live proposals and is checked before any
  store-wide bound. The store makes room by dropping settled records
  only, the proposer's own first, and never another user's live
  proposal; it then refuses with a typed quota error.
- The canonical arguments are bounded per user and in total, with a
  typed quota error.

Assisted-by: Claude:claude-sonnet-5-5
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
mudler added 29 commits October 20, 2026 07:27
The job services reach the progress and stop operations through
adapters beside the existing job adapters. The application registers
them with the same broker as the job operations, and only the owner of
an add-on host has a broker.

Assisted-by: Claude:claude-haiku-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The host registers the model through the existing export and import
services, as the calling user, and returns only the model name. The
operation takes no path. The name is optional and limited to a fixed
pattern, and the export format is limited to the values the fine-tuning
docs list.

Assisted-by: Claude:claude-haiku-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The fine-tuning and quantization services are adapted to the model
registration operations. A job the user does not hold is not found, and
a job that has not completed, or whose export or import is running, is a
conflict, since the services report these as plain errors.

Assisted-by: Claude:claude-haiku-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The export and import services refused a name only when its config file
existed. A folder or a file of another extension with the same name was
used as the output, so a registration could write into an existing
model. The check now refuses any entry with the name directly under the
models folder.

Assisted-by: Claude:claude-haiku-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
finetune.export and quantize.import need the administrator role on top
of the feature. The broker checks the role with the rule of the admin
routes; a local-owner install counts its owner as administrator. A
feature-holding user who is not an administrator gets 403.

finetune.export works from the job's latest checkpoint, the one with the
highest step. A job with no checkpoint is a conflict that names it.

The model registration adapters take small interfaces, so their 404 and
409 mapping is tested with fakes: a missing job is not found, and a job
that has not completed or whose export or import is running is a
conflict.

Assisted-by: Claude:claude-haiku-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Changelog line for the 2026-10-18 owner ruling on finetune.export and
quantize.import.

Assisted-by: Claude:claude-haiku-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The admin check of model registration now decides on the actor. With an
account database the role of the active account decides, including in
local-owner mode; the local owner counts as administrator only when no
account database exists. A name the collision check cannot match is
treated as taken, so the guard refuses it.

Assisted-by: Claude:claude-haiku-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The 2026-10-18 changelog line now says that local-owner counts as
administrator only when there is no account database.

Assisted-by: Claude:claude-haiku-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A host operation stores an inline text dataset of up to 900 KiB through
the existing dataset service and returns a reference. A start operation
resolves a reference only for the user it was issued to.

Assisted-by: Claude:claude-haiku-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
… add-ons

One source directory holds the stage and status helpers, an SSE reader
and the step, check, stage and log views. Each add-on bundles what it
imports. The SDK does not carry them.

Assisted-by: Claude:claude-sonnet-5-5 node
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The add-on builder refuses a bundle that imports a core UI file, so
tools.js, tools.css and formatBytes are copies. A node test now fails
when a copy differs from its core original. The spec changelog records
the exception. Also tighten the step-line assertion and give the plan
the glob form of the test command.

Assisted-by: Claude:claude-sonnet-5-5 node
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The fine-tuning page becomes an add-on page that calls the core routes
with the user's session. The add-on also offers tools to start, stop,
inspect and export jobs through the host operations. A model controls
only a job that a tool started.

A running job whose progress reads 0 is shown as not reported, and the
export controls are shown to administrators only.

Assisted-by: Claude:claude-sonnet-5-5 go node
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
A read tool may leave out resourceId when it answers about the calling
user. The host runs it without a resource, bound to the caller's
envelope, only when the verified manifest declares the tool that way. A
write or schema tool, and a read tool that offers resourceId, still must
require it. The argument limit of a tool call rises to the payload limit
so a tool can take a dataset of up to 900 KiB.

Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
fine-tune-list shows the jobs of the calling user, those started in the
web page included. fine-tune-dataset sends a small inline dataset after
a person confirms it and keeps the reference the host returns, which
fine-tune-start then uses as its datasetSource.

Assisted-by: Claude:claude-sonnet-5-5 go node
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The argument limit of a tool call is 1 MiB and one byte more is refused.
A second user's job list does not show the first user's jobs.

Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Policy.AdmitAddon is tested for a stale account, a held add-on, an empty
add-on ID and a missing function, and the gateway provider is checked
against the interface. The 1 MiB argument limit now counts the tool key
the host adds, so an accepted call always fits the payload limit; a call
at the limit is tested through the adapter and the envelope check.

Assisted-by: Claude:claude-sonnet-5-5 go node
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The quantization page becomes an add-on page that calls the core routes
with the user's session, and the add-on offers tools to list, start,
stop and import jobs through the host operations.

Assisted-by: Claude:claude-sonnet-5-5 go node
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The old routes open the add-on while it is installed and running and the
core page otherwise. The two Build hub tabs go; the Build overview keeps
its cards. Users find the add-ons in Browse add-ons and, once installed,
in the Add-ons drawer.

Assisted-by: Claude:claude-sonnet-5-5 node playwright
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
…-ons

The auth specs pin that a user without the fine_tuning or quantization
feature gets 403 from the job routes the add-on pages call. The docs
describe both pages as add-ons, the old links, the unchanged API and the
chat tools. The e2e spec covers the core fallback of the old quantize
link.

The suite inventory of verify.sh lacked cases that earlier commits
added (read tools without a resource, SDK refusals), so the contracts
and apps suites failed on a mismatch. List them.

Assisted-by: Claude:claude-sonnet-5-5 go playwright
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
package.sh and the release workflow read the add-on from its platform
file instead of naming Tables. The workflow runs one set of legs for
each add-on, and each add-on other than Tables publishes to its own
repository. Tables builds as before; the pure Go add-ons build without
cgo.

The platform files of the four other add-ons list every platform as
disabled, so no leg builds until a platform is enabled with its native
check.

Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The publish and promote jobs derived the repository of an add-on from
ADDON_OCI_REPOSITORY, but the documented value gave no form that the
add-on tools accept. Document the variable as the Tables repository in
the add-on namespace, and test that the derivation rule yields an
accepted repository for every add-on by running the workflow step.

Make the plan step fail on a publishing event when an add-on has no
enabled platform, and tolerate only that answer on other events. A
missing or malformed platforms.yaml now fails the plan on every event.

Check in the workflow policy that Tables keeps the configured
repository, that publish and promote do not stop each other with
fail-fast, that publish downloads only the layouts of its own add-on,
and that promote checks the digest format. Cover each with a mutation
test and run the plan step against stub platform files.

Stop asserting that platforms are disabled: a later change enables
them. State in the docs that publishing events fail closed for an
add-on until one of its platforms is enabled.

Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Voice, Face, fine-tuning and quantization each list linux and darwin on
amd64 and arm64 with the runner that checks them natively. Every
platform stays disabled until its native check passes. A test fails
when a platform is enabled without a recorded check. Each package gets a
catalog template so Browse add-ons can list it.

Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The native check passes on linux/amd64 for Voice, Face, fine-tuning and
quantization, so the platform is enabled for the four packages. The
release workflow legs on ubuntu-24.04 run the same check.

Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The packaging checks compile Go packages that import the generated gRPC
stubs. The stubs are not in the tree, so the validate job failed at
setup on every run and the package legs, which wait for it, never ran.
Generate them with the pinned tools the test workflow uses.

Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The native check passes on ubuntu-24.04-arm for Voice, Face, fine-tuning
and quantization, so the platform is enabled for the four packages.

Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The previous commit left a stray line in each platform file, which
made the files unreadable. Enable linux/arm64 with a plain edit.

Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The native check passes on macos-14 for Voice, Face, fine-tuning and
quantization, so the platform is enabled for the four packages.

Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
The package workflow runs the native check on macos-15-intel for Voice,
Face, fine-tuning and quantization, so the platform is enabled for the
four packages.

Assisted-by: Claude:claude-sonnet-5-5 go
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
@mudler
mudler force-pushed the feat/addons-tables branch from 05f89f0 to 0d71c38 Compare October 20, 2026 07:28

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants