> ## Documentation Index
> Fetch the complete documentation index at: https://starforge.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugin packages

> The manifest, the directory layout, and the publish-install-reference cycle.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf plugin publish ./my-plugin
```

A plugin is a directory with a `plugin.yaml` in it. That is the whole packaging format — no build
step, no wheel, no registry account.

## Layout

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
my-plugin/
├── plugin.yaml       # the manifest, required
├── README.md         # long description, optional, shown on the detail page
└── <your files>      # code for executable kinds, declarations for the rest
```

## plugin.yaml

```yaml plugin.yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
schema: forge/plugin/v1
name: opsd-patch
version: 1.2.0
kind: algorithm
entrypoint: patch:install
load: eager
summary: OPSD teacher-sampling correction

requires:
  core: ">=0.3,<1"

# Shelf metadata — display and search only, never affects loading or security
icon: 🧩
author: Alice
license: Apache-2.0
homepage: https://github.com/alice/opsd-patch
keywords: [opsd, distillation]
```

### Required fields

<ParamField path="schema" type="string" required>
  Always `forge/plugin/v1`. Any other value is refused with the version this build speaks.
</ParamField>

<ParamField path="name" type="string" required>
  The leaf name. The platform stamps your account on it at publish time, so the published id is
  `<owner>/<name>` — you do not write the owner yourself.
</ParamField>

<ParamField path="version" type="string" required>
  Starts with a letter or digit; letters, digits, `.`, `_`, `-` after that. Immutable once published.
</ParamField>

<ParamField path="kind" type="string" required>
  One of `algorithm`, `environment`, `data-prep`, `recipe`, `benchmark`, `playbook`, `prompt`.
  Decides everything else about how the package is treated.
</ParamField>

### Required for executable kinds

<ParamField path="entrypoint" type="module.path:callable">
  Required for `algorithm` and `environment`. The module path is relative to the package root. Must
  exist at publish time.

  Forbidden for every other kind. A `data-prep` plugin is discovered by the `prepare_*.py`
  convention; a declarative pack executes nothing, so it has no entry point at all.
</ParamField>

<ParamField path="load" type="eager | deferred" default="eager">
  When the launcher calls your entrypoint. See [algorithm plugins](/en/extend/algorithm-plugins).
</ParamField>

### Optional fields

<ParamField path="requires.core" type="PEP 440 specifier">
  Which `starforge-core` versions your plugin works with, for example `">=0.3,<1"`. Checked at
  launch; a mismatch stops the job with `plugin x@1.2.0 requires SDK >=0.3,<1, this one is 0.2.9`
  rather than failing somewhere stranger later.
</ParamField>

<ParamField path="summary" type="string">
  One line, shown in listings.
</ParamField>

<ParamField path="icon" type="emoji">
  A single emoji for the shelf card. At most 8 bytes. Defaults to the kind's icon.
</ParamField>

<ParamField path="author" type="string">
  Who wrote it. Distinct from `owner`, which is the platform account that published it.
</ParamField>

<ParamField path="license, homepage, keywords" type="string, url, string[]">
  Display and search only. `homepage` must be `http(s)`. At most 12 keywords, lowercased and
  deduplicated.
</ParamField>

### README.md

A `README.md` at the package root becomes the long description on the detail page. Capped at 64 KB;
anything beyond is truncated, because the detail page wants an introduction, not a manual.

It is deliberately not a manifest field — long text in YAML forces the author to escape things and
loses the Markdown toolchain. It is covered by the package digest, so changing it needs a new
version.

## The seven kinds

| Kind          | Executes | Where                   | Entrypoint                     | Who may publish |
| ------------- | -------- | ----------------------- | ------------------------------ | --------------- |
| `algorithm`   | Yes      | Your training container | Required                       | Anyone          |
| `environment` | Yes      | Your training container | Required                       | Anyone          |
| `data-prep`   | Yes      | Your machine            | No — `prepare_*.py` convention | Anyone          |
| `recipe`      | No       | —                       | Forbidden                      | Anyone          |
| `benchmark`   | No       | —                       | Forbidden                      | Anyone          |
| `playbook`    | No       | —                       | Forbidden                      | **Admin only**  |
| `prompt`      | No       | —                       | Forbidden                      | **Admin only**  |

<Info>
  `playbook` and `prompt` are deployment-wide singletons: one is active and it changes what everyone
  sees on a diagnosis page. They are admin-only because the consequence lands on other people. There
  are no per-user versions — "whose diagnosis verdict is this" is a question that should not exist.
</Info>

## Publish

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf plugin publish ./my-plugin
sf plugin publish ./my-plugin --owner platform-team   # admins only
```

The client validates the manifest and the layout locally, computes the directory digest, and
uploads. The server recomputes the digest and refuses a mismatch.

| Failure | Meaning                                                       |
| ------- | ------------------------------------------------------------- |
| `409`   | This version already exists. Versions are immutable — bump it |
| `400`   | Manifest or layout problem; `detail` names the field          |
| `403`   | Publishing into a namespace that is not yours                 |

## Install and reference

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf plugin ls                                      # what this deployment has
sf plugin info alice/opsd-patch                   # manifest, versions, digest
sf plugin install alice/opsd-patch                # download into ./forge_plugins/
sf plugin install alice/opsd-patch --exp my-grpo  # and lock it to an experiment
```

`--exp` writes `plugins.lock.json` into the experiment: the reference and its digest. At submission
the platform reads that lockfile, injects exactly that version into the job package under
`forge_plugins/<name>/`, and records the triple in the JobSpec.

```json plugins.lock.json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "plugins": [
    { "id": "alice/opsd-patch", "version": "1.2.0", "digest": "sha256:9f2c…" }
  ]
}
```

<Warning>
  Two plugins whose packages would unpack into the same leaf directory name cannot be combined in one
  job. Rename one.
</Warning>

## Confirm it worked

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf plugin info alice/opsd-patch
```

Shows the version you just published and its digest. After submitting a job that references it, the
job log carries one line per loaded plugin:

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
plugin  : alice/opsd-patch@1.2.0 loaded
```

If the digest was tampered with after injection, the job refuses to start instead:

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
plugin alice/opsd-patch@1.2.0 content digest mismatch: spec=sha256:9f2c…, actual=sha256:41ab…
```

## Next

<Columns cols={2}>
  <Card title="Algorithm plugins" icon="code" href="/en/extend/algorithm-plugins" arrow="true">
    What your entrypoint receives, and when it is called.
  </Card>

  <Card title="Recipe packs" icon="book-marked" href="/en/extend/recipe-packs" arrow="true">
    A training method as a declaration.
  </Card>
</Columns>
