diff --git a/addons/internal/connector-ui/src/connector.js b/addons/internal/connector-ui/src/connector.js
index b68bbf215c..2aaef39d39 100644
--- a/addons/internal/connector-ui/src/connector.js
+++ b/addons/internal/connector-ui/src/connector.js
@@ -70,7 +70,7 @@ export function formatBytes(n) {
// The body of a create or replace request for a binding of kind. token is the
// secret the add-on's fields make; it goes only when it was typed: an update
// without one keeps the stored token.
-export function connectorPayload(form, kind, token = form.token) {
+export function connectorPayload(form, kind, token = form.token, extra = {}) {
const body = {
kind,
name: (form.name || '').trim(),
@@ -90,7 +90,9 @@ export function connectorPayload(form, kind, token = form.token) {
if (body.conversation === 'window') body.window_minutes = Number(form.window_minutes) || 0
const secret = (token || '').trim()
if (secret) body.token = secret
- return body
+ // Fields of the add-on's own (a list of resources) and overrides, such as a
+ // binding that carries no messages and so is never switched on.
+ return { ...body, ...extra }
}
// Who the binding accepts, in a few words.
diff --git a/core/services/addons/catalog/official_test.go b/core/services/addons/catalog/official_test.go
index f61b5c5dde..a1e33a72e3 100644
--- a/core/services/addons/catalog/official_test.go
+++ b/core/services/addons/catalog/official_test.go
@@ -92,7 +92,7 @@ var _ = Describe("official library index", func() {
Expect(versionPattern.MatchString(r.Version)).To(BeTrue(), r.ID)
Expect(r.Artifact).To(HavePrefix("oci://"), r.ID)
}
- Expect(slices.Compact(slices.Sorted(slices.Values(ids)))).To(ConsistOf("fine-tuning", "quantization", "voice", "face", "studio", "talk", "telegram", "slack", "tables", "office", "sandbox", "web", "memory"))
+ Expect(slices.Compact(slices.Sorted(slices.Values(ids)))).To(ConsistOf("fine-tuning", "quantization", "voice", "face", "studio", "talk", "telegram", "slack", "github", "tables", "office", "sandbox", "web", "memory"))
})
})
diff --git a/docs/content/features/addons.md b/docs/content/features/addons.md
index 7a99d3ba39..7718b5639d 100644
--- a/docs/content/features/addons.md
+++ b/docs/content/features/addons.md
@@ -493,6 +493,45 @@ own record of the grants.
- An add-on that declares no `list` gets no listing tool, whatever else it
declares.
+### Credentials and a resource list
+
+An add-on whose tools call a service with a secret of the person, such as an
+access token, can ask the host to keep the secret and to check which resources
+a call may reach. The model never sees or sets either.
+
+```json
+{
+ "api": {"min": 2, "max": 2},
+ "capabilities": {
+ "credentials": [
+ {"id": "token", "title": "Access token", "kind": "token", "resourceArgument": "repo"}
+ ],
+ "connector": {"kinds": [{"id": "example", "title": "Example", "delivery": "pull"}]}
+ }
+}
+```
+
+- The secret and the list live in the person's **binding** of the add-on's
+ connector kind. The host seals the secret. The list is `Binding.Resources`: at
+ most 50 entries, each an external name (`owner/name` for a repository) with a
+ host ID made from its hash. The person edits it where they edit the binding.
+- `resourceArgument` names a string argument that carries the external name. A
+ tool that declares it must require it. Before the add-on is called, the host
+ matches the argument against the lists of the caller's bindings of the add-on,
+ without regard to case. A name on no list is refused with the code
+ `resource_not_allowed` and the allowed names; the add-on is not called. When
+ two bindings list the same name, the first in list order is used.
+- A call that passes the check carries `Request.Credential` (the secret),
+ `Request.ResourceKey` (the name as the person wrote it) and `Request.ResourceID`
+ (the host ID). The host drops these three fields from any request a client
+ sends. A person with no binding gets the code `no_credential`.
+- A manifest with `credentials` must accept `api` 2, declare a connector kind and
+ declare read tools only: a change under a credential needs a write mode, which
+ this version does not have. Tools of such an add-on name no `resourceId`.
+- An add-on that is rate limited returns `contract.ErrRateLimited{RetryAfter}`.
+ The runtime answers HTTP 429 with `Retry-After`; the model gets the code
+ `rate_limited`, `retryAfter` in seconds and a note not to retry before then.
+
## Singleton owner and its limits
Addon state lives in one place. Exactly one LocalAI instance is the **owner** of an
diff --git a/docs/content/features/chats-connectors.md b/docs/content/features/chats-connectors.md
index cb50c246fe..3d056c67d0 100644
--- a/docs/content/features/chats-connectors.md
+++ b/docs/content/features/chats-connectors.md
@@ -7,7 +7,7 @@ url = "/features/chats-connectors/"
The Connectors page of the [chats](/features/chats/) (`/app/chats/connectors`, under **Agents** in the sidebar) lists the messaging bindings you made, and the MCP servers have a page of their own (**MCP connectors** in the sidebar, `/app/mcp-connectors`):
-- A **messaging connector** binds a bot of a platform to one of your agents, so a person can talk to it from that platform. The platform comes from a **connector add-on**: the [Telegram add-on](#telegram) or the [Slack add-on](#slack). This is the first half of this page.
+- A **messaging connector** binds a bot of a platform to one of your agents, so a person can talk to it from that platform. The platform comes from a **connector add-on**: the [Telegram add-on](#telegram) or the [Slack add-on](#slack) (the [GitHub add-on](#github) holds a token for your agents instead of carrying messages). This is the first half of this page.
- An **MCP server** gives your chats and agents more tools. See [MCP servers](#mcp-servers).
## Connector add-ons
@@ -134,6 +134,20 @@ speech-to-text model is set up") and no turn starts.
binding. `POST .../enable` and `.../disable` switch it. You can have 10 bindings.
A binding says `active: false` and gives a `reason` while its add-on does not run.
+## GitHub
+
+The GitHub add-on lets your [saved agents](/features/chats-agents/) read repositories: search code, read files and folders, and read issues, pull requests and their diffs. It reads only. It cannot write, merge or delete anything on GitHub.
+
+1. Install the **GitHub** add-on from the add-on gallery.
+2. On GitHub, make a **fine-grained personal access token** for only the repositories you want to share. Give it read access to Contents, Issues and Pull requests. Classic tokens are not accepted.
+3. On the add-on page, choose **New GitHub connection**. Pick an agent, paste the token and list the repositories, one `owner/name` per line (at most 50). Press **Test** to see whose token it is and which listed repositories it reaches.
+
+The list is checked by LocalAI before the add-on is called, so an agent is refused any repository that is not on it, even if the token could reach it. The token is kept sealed and is never shown again, and no tool result contains it. With several connections, a call uses the first one that lists the repository.
+
+What GitHub returns (issue text, comments, file content) is written by other people. Every result is marked as untrusted data for the model. Long files are cut at 96 KiB and long patches at 64 KiB; the result says what was cut and how to ask for the rest. When GitHub limits the rate of calls, the agent is told how long to wait.
+
+This version does not listen for mentions and has no write tools.
+
## Slack
The Slack add-on binds a Slack app to one of your [saved agents](/features/chats-agents/).
diff --git a/gallery/addons/README.md b/gallery/addons/README.md
index 85460aa73a..501d106921 100644
--- a/gallery/addons/README.md
+++ b/gallery/addons/README.md
@@ -3,7 +3,7 @@
This directory holds the template of the catalog entries of the first-party add-ons.
It holds no live URL, registry or namespace. The registry and repository are inputs of
the release workflow (`ADDON_OCI_REPOSITORY`). Each first-party add-on has its own
-template (`tables`, `voice`, `face`, `fine-tuning`, `quantization`, `studio`, `talk`, `office`, `sandbox`, `web`, `memory`) and its own release
+template (`tables`, `voice`, `face`, `fine-tuning`, `quantization`, `studio`, `talk`, `office`, `sandbox`, `web`, `memory`, `github`) and its own release
platform matrix, `addons//package/platforms.yaml`. The templates differ in `name:` only.
## Generate the index
diff --git a/gallery/addons/github.yaml.tmpl b/gallery/addons/github.yaml.tmpl
new file mode 100644
index 0000000000..c080cbf2b1
--- /dev/null
+++ b/gallery/addons/github.yaml.tmpl
@@ -0,0 +1,18 @@
+id: "{{addon}}"
+name: GitHub
+description: "{{description}}"
+version: "{{version}}"
+channel: "{{channel}}"
+artifact: "oci://{{repository}}:{{tag}}"
+digest: "{{digest}}"
+os: "{{os}}"
+arch: "{{arch}}"
+policy:
+ requireSignature: true
+ issuer: "{{issuer}}"
+ identity: "{{identity}}"
+ui:
+ title: "{{title}}"
+ description: "{{description}}"
+ mainPage: "{{main_page}}"
+ icon: "{{icon}}"
diff --git a/gallery/addons/index.yaml b/gallery/addons/index.yaml
index 89a86c474c..a110e67a1f 100644
--- a/gallery/addons/index.yaml
+++ b/gallery/addons/index.yaml
@@ -194,6 +194,118 @@
- "fine-tune-checkpoints"
- "fine-tune-stop"
- "fine-tune-export"
+- id: "github"
+ name: GitHub
+ description: "Let your agents read the code, issues and pull requests of the repositories you choose."
+ version: "0.0.0-development"
+ channel: "development"
+ artifact: "oci://quay.io/go-skynet/local-ai-addons:github-development"
+ mutable: true
+ os: "darwin"
+ arch: "amd64"
+ policy:
+ requireSignature: true
+ issuer: "https://token.actions.githubusercontent.com"
+ identity: "https://github.com/mudler/LocalAI/.github/workflows/addons-package.yml@refs/heads/master"
+ ui:
+ title: "GitHub"
+ description: "Let your agents read the code, issues and pull requests of the repositories you choose."
+ mainPage: "apps"
+ icon: "ui/icon.png"
+ toolCount: 8
+ tools:
+ - "search-code"
+ - "read-file"
+ - "list-tree"
+ - "list-issues"
+ - "get-issue"
+ - "get-pr"
+ - "get-pr-diff"
+ - "check"
+- id: "github"
+ name: GitHub
+ description: "Let your agents read the code, issues and pull requests of the repositories you choose."
+ version: "0.0.0-development"
+ channel: "development"
+ artifact: "oci://quay.io/go-skynet/local-ai-addons:github-development"
+ mutable: true
+ os: "darwin"
+ arch: "arm64"
+ policy:
+ requireSignature: true
+ issuer: "https://token.actions.githubusercontent.com"
+ identity: "https://github.com/mudler/LocalAI/.github/workflows/addons-package.yml@refs/heads/master"
+ ui:
+ title: "GitHub"
+ description: "Let your agents read the code, issues and pull requests of the repositories you choose."
+ mainPage: "apps"
+ icon: "ui/icon.png"
+ toolCount: 8
+ tools:
+ - "search-code"
+ - "read-file"
+ - "list-tree"
+ - "list-issues"
+ - "get-issue"
+ - "get-pr"
+ - "get-pr-diff"
+ - "check"
+- id: "github"
+ name: GitHub
+ description: "Let your agents read the code, issues and pull requests of the repositories you choose."
+ version: "0.0.0-development"
+ channel: "development"
+ artifact: "oci://quay.io/go-skynet/local-ai-addons:github-development"
+ mutable: true
+ os: "linux"
+ arch: "amd64"
+ policy:
+ requireSignature: true
+ issuer: "https://token.actions.githubusercontent.com"
+ identity: "https://github.com/mudler/LocalAI/.github/workflows/addons-package.yml@refs/heads/master"
+ ui:
+ title: "GitHub"
+ description: "Let your agents read the code, issues and pull requests of the repositories you choose."
+ mainPage: "apps"
+ icon: "ui/icon.png"
+ toolCount: 8
+ tools:
+ - "search-code"
+ - "read-file"
+ - "list-tree"
+ - "list-issues"
+ - "get-issue"
+ - "get-pr"
+ - "get-pr-diff"
+ - "check"
+- id: "github"
+ name: GitHub
+ description: "Let your agents read the code, issues and pull requests of the repositories you choose."
+ version: "0.0.0-development"
+ channel: "development"
+ artifact: "oci://quay.io/go-skynet/local-ai-addons:github-development"
+ mutable: true
+ os: "linux"
+ arch: "arm64"
+ policy:
+ requireSignature: true
+ issuer: "https://token.actions.githubusercontent.com"
+ identity: "https://github.com/mudler/LocalAI/.github/workflows/addons-package.yml@refs/heads/master"
+ ui:
+ title: "GitHub"
+ description: "Let your agents read the code, issues and pull requests of the repositories you choose."
+ mainPage: "apps"
+ icon: "ui/icon.png"
+ toolCount: 8
+ tools:
+ - "search-code"
+ - "read-file"
+ - "list-tree"
+ - "list-issues"
+ - "get-issue"
+ - "get-pr"
+ - "get-pr-diff"
+ - "check"
- id: "memory"
name: Short memory
description: "Your chats keep short notes about you, read them before they answer, and save a new one only after you confirm it."
diff --git a/pkg/addons/packaging/packaging_test.go b/pkg/addons/packaging/packaging_test.go
index 608fb1c747..1922d85eaf 100644
--- a/pkg/addons/packaging/packaging_test.go
+++ b/pkg/addons/packaging/packaging_test.go
@@ -916,7 +916,7 @@ var _ = Describe("catalog template", func() {
Expect(err).To(MatchError(ContainSubstring("development channel only")))
})
It("renders an entry for every first-party add-on from its own template", func() {
- for _, id := range []string{"tables", "voice", "face", "fine-tuning", "quantization", "studio", "talk", "telegram", "slack", "office", "sandbox", "web", "memory"} {
+ for _, id := range []string{"tables", "voice", "face", "fine-tuning", "quantization", "studio", "talk", "telegram", "slack", "github", "office", "sandbox", "web", "memory"} {
p := newProjectFor(id, both)
m := p.merged("1.2.3", rev, nil)
repoName := newRegistry().repo("addons")
@@ -1013,7 +1013,7 @@ var _ = Describe("catalog template", func() {
for _, e := range entries {
got[e.ID] = e.ToolCount
}
- Expect(got).To(Equal(map[string]int{"tables": 16, "fine-tuning": 8, "quantization": 6, "voice": 0, "face": 0, "studio": 0, "talk": 0, "telegram": 0, "slack": 0, "office": 11, "sandbox": 5, "web": 5, "memory": 0}))
+ Expect(got).To(Equal(map[string]int{"tables": 16, "fine-tuning": 8, "quantization": 6, "voice": 0, "face": 0, "studio": 0, "talk": 0, "telegram": 0, "slack": 0, "github": 8, "office": 11, "sandbox": 5, "web": 5, "memory": 0}))
})
It("refuses missing inputs, backend repositories, bad channels and unknown placeholders", func() {
reg := newRegistry()
@@ -1175,7 +1175,7 @@ var _ = Describe("workflow policy", func() {
})
It("rejects a workflow that names one add-on by path or skips an add-on in the matrix", func() {
expect(mutate("node \"addons/$ADDON/ui/build.mjs\"\n - name: Fill", "node addons/tables/ui/build.mjs\n - name: Fill"), "P_ADDON_PATH")
- expect(mutate(" addon: [tables, voice, face, fine-tuning, quantization, studio, talk, telegram, slack, office, sandbox, web, memory]\n", " platform: [x]\n"), "P_MATRIX")
+ expect(mutate(" addon: [tables, voice, face, fine-tuning, quantization, studio, talk, telegram, slack, github, office, sandbox, web, memory]\n", " platform: [x]\n"), "P_MATRIX")
})
It("rejects a plan that does not refuse an add-on without an enabled platform", func() {
expect(mutate("has no enabled platform; refusing to publish", "has no platform"), "P_FAIL_CLOSED")
@@ -1225,7 +1225,7 @@ var _ = Describe("workflow policy", func() {
// firstPartyAddons are the add-ons the release workflow packages. The list is
// written out so a new add-on is a deliberate change here and in the workflow.
-var firstPartyAddons = []string{"tables", "voice", "face", "fine-tuning", "quantization", "studio", "talk", "telegram", "slack", "office", "sandbox", "web", "memory"}
+var firstPartyAddons = []string{"tables", "voice", "face", "fine-tuning", "quantization", "studio", "talk", "telegram", "slack", "github", "office", "sandbox", "web", "memory"}
func readWorkflow() string {
GinkgoHelper()