---
title: "Install from a source"
description: "Choose a folder, standalone agent or private repository and meet the source-fetch rules."
canonical_url: "https://docs.oz0.ai/build/plugins/install/sources"
---
# Install from a source

> Choose a folder, standalone agent or private repository and meet the source-fetch rules.

For: tenant administrators and plugin authors.

Choose a folder, standalone agent or private repository and meet the source-fetch rules.

## A plugin in a folder

A repository can hold several plugins, each in its own folder with its own `oz0-plugin.yaml`. Name the folder with `--path`:

```bash [Terminal]
oz0 plugin install https://github.com/acme/agents --path plugins/github-tools
```

The ref still applies to the whole repository: the edge locks the commit, then packs only that folder, so the plugin sees the folder as its root and nothing else from the repository reaches the nodes. The path cannot leave the repository, and a folder that is a symlink does not count as one.

## An agent in one file

A Markdown file that is a plugin of one part, such as an agent in one file that `oz0 agent new` writes, installs from the file itself:

```bash [Terminal]
oz0 plugin install ./triage.md
oz0 run triage "Triage issue 12 of acme/api"
```

The file is packed alone, without what lies beside it, and uploaded; its manifest is generated, named after the file, with the version `0.0.0-d<digest>` of what was uploaded.

On a terminal the install screen asks once, and yes also grants what the agent uses of other plugins; in a script the install takes the defaults, which grant nothing, and says the `--accept-uses` to give.

`oz0 plugin update triage` uploads the file again: a changed file is a new version. When it needs code, evals or more parts, `oz0 plugin eject ./triage.md` writes the full folder, and installing that folder updates the same plugin ([Agents](https://docs.oz0.ai/build/parts/agents#an-agent-in-one-file)).

## A private repository

The edge fetches a private repository over HTTPS with a token you keep as a [tenant secret](https://docs.oz0.ai/admin/secrets), bound to the
Git host it is for. Store the token with `--host`, then name the secret with `--secret`:

```bash [Terminal]
oz0 secret set GITHUB_TOKEN --from-file ~/.secrets/github-token --host github.com --host api.github.com
oz0 plugin install https://github.com/acme/private-tools --secret GITHUB_TOKEN
```

The secret holds the token alone, or `user:token` for Git servers that need a particular user name. A token alone goes as the user `x-access-token`, which GitHub accepts for personal access tokens and app tokens alike. Give it read access to that repository only: on GitHub, a fine-grained token with *Contents: read*.

The edge unseals the token for the fetch only when the repository's host is one the secret is bound to, and hands it to `git` in its environment, never on a command line, only for that host, over HTTPS, with redirects turned off (a Git server that redirects an authenticated fetch is refused: name its final URL). A secret bound to no host, or to others, is refused before anything is sent, with the command that binds it; whoever may install plugins can name a secret, but only a holder of `secrets.write` decides where it may go.

An OCI registry's credentials go the same way: to the registry and to the token service its challenge names only when each host is bound (Docker Hub's are `registry-1.docker.io` and `auth.docker.io`), and never on a redirect to another host. A marketplace's token goes to the marketplace's host, and on to an entry only on the same host. The plugin remembers the secret's name, never its value, so `plugin update` fetches the same way; after you rotate the token with `secret set`, updates use the new one and the binding stays.

<note>

SSH URLs and deploy keys are not supported yet; use an HTTPS URL with a token.

</note>

## Where the edge fetches from

A plugin's source, and every address its content names (a marketplace's entries, a listed version's bundle, an OCI registry and its token service, a tool's image, a remote tool's address for the sign-in check, a package index), is someone else's choice, not yours as the operator. The edge fetches all of them under one rule:

- Public addresses, over HTTPS. Plain `http://` is refused outside dev mode, for Git too: anyone between the edge and the Git server could change which commit it fetches.
- Never the edge's own machine, its link or a cloud's metadata service: loopback, `0.0.0.0`, link-local addresses such as `169.254.169.254`, the clouds' other metadata addresses, multicast, and the IPv6 forms that carry such an address, whether the URL names it, a name resolves to it, or a redirect leads there. The addresses of the edge's own network interfaces are refused too, inside a listed network as well, since every service on the edge that listens on all addresses answers on them; they are read at each connection, so an address that DHCP or a VPN adds later counts at once.
- Private networks when you list them. A Git server, registry, forge, plugin index or package mirror on a private network (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, carrier-grade NAT's `100.64.0.0/10`, IPv6 unique local addresses) or a reserved range is reached once the edge starts with its network listed, `oz0 server start --plugin-source-allow-net 10.20.0.0/16` (repeatable). Until then the refusal names the flag.

### Checked where it connects

The edge resolves a Git server's name once, refuses it when any address it resolves to is refused, and holds `git` to exactly those addresses, with no redirect followed (a Git server that redirects is refused: name the URL it redirects to); this needs git 2.37 or later on the edge. Every other fetch checks the address of each connection it makes, every redirect included; a plugin index and a package index follow redirects on their own host only, a forge's API and the sign-in check none.

Through an HTTP proxy you set (`HTTPS_PROXY`), the edge checks the addresses the name resolves to first, and the proxy resolves it again under its own rules.

- No credentials in a URL. A source, repository or forge URL with a user name or password in it is refused before anything is fetched, stored or audited: it would be kept with the plugin, shown to every reader of the tenant and written to the audit log. Keep the token in a secret bound to the host, as above.

### Bounded while it is fetched

A Git build holds the repository's commit to 1 GB on disk while git writes it, its files to 512 MB and 200,000 entries before they are checked out, a ref listing to 8 MB, a marketplace's list to 8 MB and the plugin's artifact to 64 MB; at most four Git builds check out and check their commits at once, and at most eight builds of any kind are in progress, two of them for one tenant. When git fails, the error says what failed in the edge's words; git's own message goes to the server's log.

`git` runs with only what it needs of the edge's environment (the path, the locale, the time zone, the temporary folder, the certificates and the proxies), never the database URLs, the advisory feed's token or anything else the server was given there, and with an empty home folder of its own, so a `.netrc`, git configuration or ssh key in the server account's home never goes to a host a tenant names.

### Fair between tenants

A build may wait on a server a tenant admin names, which may answer slowly on purpose, so it never holds a slot the other tenants' builds need. Each build holds one of the edge's eight build slots, of which one tenant holds at most two, from before the edge reads anything of it (an upload's bytes included) to its last check: however its servers stall, a tenant's builds hold at most two bundles in memory or two Git work folders on disk at once, and its other builds wait before they read anything.

A build takes one of the edge's four shared slots only for the work it does with bytes that are in (the checkout and the checks), and gives it back for every wait on such a server (fetching the commit and the commits a merge commit brought in, mirroring its tools' OCI images, asking the forge for the repository's ID, asking a remote tool whether it wants a sign-in, reading the package indexes). An OCI image mirror is held to one at a time for each tenant.

Every build on a server names the tenant it is for; one that names none is refused.

### What was checked is what is installed

The artifact is the plugin's folder as the checks read it: a repository's `.gitattributes` cannot leave a file out of it (`export-ignore`) or change one as it is packed (`export-subst`, line endings, filters), and the edge compares the artifact with the checked files before it uses it. Two files whose names differ only by letter case are refused, by the edge and by every node, since a case-insensitive file system stores them as one file; a node also never writes a file over one already there.

Dev mode reaches every address, over plain `http://` too, as before. A cluster started with `--allow-file-plugins` (for tests) installs from `file://` sources and folders and fetches plain `http://`, and keeps every other part of this rule: it never reaches the edge's own machine, its link or a cloud's metadata service.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
