Pistis

Plugins

Add a code host, sign-in method, sandbox provider or policy as a package.

A plugin adds a new kind of code host, sign-in method, sandbox provider or policy. Admins then set it up in the web app like the built-in ones, which use the same contract.

Write one

A plugin adds kinds. A kind is one type of code host, sign-in method, sandbox provider or policy: a JSON Schema for its settings and a create(settings) function.

npm i @pistishq/sdk
import { definePlugin } from "@pistishq/sdk";
import type { CodeHostKind } from "@pistishq/sdk/scm";

const forge: CodeHostKind<{ url: string; token: string }> = {
  kind: "acme-forge",                        // stored on every instance: never rename
  label: "Acme Forge",
  config: {
    type: "object", additionalProperties: false, required: ["url", "token"],
    properties: {
      url: { type: "string", format: "uri", title: "Forge URL" },
      token: { type: "string", writeOnly: true, title: "Token" },   // encrypted, never shown again
    },
  },
  create: (c) => ({ /* repos, repo, gitCredential, changeRequest? */ }),
};

export default definePlugin({ name: "@acme/pistis-forge", codeHosts: [forge] });

A plugin can fill five slots: signIn, codeHosts, boxes, authorizers and secretStores. Settings are flat: strings, numbers, booleans and lists of strings.

Install it

  1. Install the package next to your pistis.json.
  2. List it in the config:
pistis.json
{ "plugins": ["@acme/pistis-forge"] }
  1. Restart Pistis. Its kinds appear in the web app.

A plugin built for another version, or a kind name already taken, stops the start with the reason. With pistis dev, use --plugin @acme/pistis-forge.

Secrets

Settings marked writeOnly show as password fields and are never filled back in. Pistis encrypts them with AES-256-GCM under PISTIS_SECRET_KEY, tied to their row and field, so a value copied elsewhere doesn't open. A setting that won't open is skipped and logged, never half-applied.

Policies

An authorizer decides what each person may do. A plugin's authorizer runs after the built-in one and can only take permissions away. An error from it counts as a refusal.

Secret stores

People's own credentials for MCP servers (MCP servers) live in a secret store. The built-in one, sealed, keeps them in Pistis's database, encrypted under PISTIS_SECRET_KEY; pistis rotate-secret re-encrypts it. A plugin can offer a vault instead: a kind in secretStores whose create returns { put(key, value) → { key, version }, get({ key, version }), delete(key), check() }. The store only keeps versioned bytes; Pistis tracks which version each record uses. Pick it with "secrets": { "kind": "vault", "config": { … } } in pistis.json. Any failure to reach the store fails closed: the call that needed the secret says so and sends nothing, and a store whose check() fails at start stops pistis serve with a generic error that omits potentially secret-bearing upstream text.

The reference store, vault, is built in: HashiCorp Vault's KV v2 engine. It was once the separate package @pistishq/secrets-vault; a plugins entry naming it is still accepted and means the built-in. Pick it in the config:

pistis.json
"secrets": { "kind": "vault", "config": { "address": "https://vault.example.com", "token": "${VAULT_TOKEN}", "mount": "secret", "path": "pistis" } }

Each Pistis secret is one KV secret under path and every change a new version of it; removing a credential deletes the secret's metadata, every version with it. A namespace (Vault Enterprise) is sent as X-Vault-Namespace. The token needs create and read on <mount>/data/<path>/*, delete on <mount>/metadata/<path>/*, and read on auth/token/lookup-self, which the start's check calls. Nothing is cached: Vault answers every call that needs a secret.

On this page