OpenLeash
Building plugins

Build a container plugin

From a tiny HTTP handler to a signed, isolated OCI image—with conversation context and optional private databases.

Your Five-Minute Loop

Start from the runnable example. One command validates it, builds the image, tests the real signed protocol, and leaves the image ready for the desktop.

cd examples/container-plugin
npm install
npm run smoke

# In Individual Open Source:
Plugins → Add/reload local folder

# Iterate:
edit → check → image → reload folder → trigger event

Check

Validates required manifest fields, container protocol settings, subscribed events, permissions, and JavaScript syntax.

Smoke test

Builds and starts the image with runtime restrictions, verifies health and HMAC signing, and round-trips conversation context.

Reload

Rebuild the same local tag and choose the folder again. Desktop detects the new image and replaces the development container.

One Protocol, Any Language

OpenLeash pulls a digest-pinned OCI image and calls a small HTTP API. It never builds plugin source code while an agent request is waiting.

OpenLeash event
  -> authenticated runtime router
  -> isolated plugin container
  -> openleash-container-plugin.v1 response

Required:
GET  /healthz
POST /v1/events

Optional:
POST /v1/transform
POST /v1/tools/execute

Correlated

Every response echoes the exact protocol and requestId. OpenLeash rejects mismatched or stale responses.

Signed

Validate the timestamp and HMAC signature before parsing or acting on an invocation.

Contained

Run non-root with a read-only root filesystem, explicit resources, network policy, and no Docker or Kubernetes socket.

Minimal Dockerfile

The application listens on port 8080, writes only to /data or /tmp, and includes both an HTTP health endpoint and OCI health check.

FROM node:22-alpine
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY server.mjs ./

RUN mkdir -p /data && chown -R node:node /app /data
USER node

ENV NODE_ENV=production
ENV PORT=8080
EXPOSE 8080
VOLUME ["/data"]

HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
  CMD node -e "fetch('http://127.0.0.1:8080/healthz').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"

CMD ["node", "server.mjs"]

Event API

The host supplies plugin identity, trusted tenant context, resolved settings, input, and results from requested host capabilities.

POST /v1/events

{
  "protocol": "openleash-container-plugin.v1",
  "requestId": "8fe6...",
  "round": 0,
  "plugin": {
    "id": "acme.history-aware",
    "version": "1.0.0"
  },
  "tenant": {
    "organizationId": "trusted-context",
    "userId": "trusted-context"
  },
  "event": "prompt.beforeSubmit",
  "settings": {
    "profileIds": [],
    "configHash": "..."
  },
  "config": {},
  "input": {},
  "capabilityResults": {}
}

Completed Response

Return typed output only. Plugins do not call desktop UI, OpenLeash tables, or provider credentials directly.

{
  "protocol": "openleash-container-plugin.v1",
  "requestId": "8fe6...",
  "status": "completed",
  "output": {
    "status": "passed",
    "summary": "Event processed.",
    "findings": []
  }
}

Ask For Conversation Context

Do not build a second conversation database. Request a bounded window from the authenticated current session.

{
  "protocol": "openleash-container-plugin.v1",
  "requestId": "8fe6...",
  "status": "capability_required",
  "capabilityRequests": [{
    "id": "context.conversation.recent:0",
    "capability": "context.conversation.recent",
    "request": { "limit": 20 }
  }]
}

Useful by default

The normalized event already carries its transcript when the agent transport provides one.

Host scoped

The container cannot select another organization, user, or arbitrary session.

Optional storage

Keep capabilities.storage for small plugin-owned values such as checkpoints or notification deduplication.

Isolation Is The Data Boundary

A userId field does not make unreviewed shared code safe. OpenLeash chooses the workload boundary before the plugin receives traffic.

shared-trustedReviewed first-party/stateless worker

Warm shared replicas; durable user data only through host-scoped capabilities.

user-dedicatedCommunity/private plugin or custom database

One user + plugin + version workload, route, secrets, persistent volume, and database role.

tenant-dedicatedOrganization-owned private plugin

One organization-bound workload and storage boundary.

customer-hostedPrivate Cloud

The customer operates the workload, database, backups, and policy.

A community image is never promoted to shared-trusted automatically. A user-dedicated image is built and published once by the developer. Enabling it creates its route, secret, persistent volume and running pod. Desktop login/presence prewarms it before agent traffic. An event normally only routes to a ready pod; starting from an event is a bounded recovery fallback.

When the user has no connected desktop and no plugin activity for an operator-defined grace period, the pod may stop while its volume remains. The next desktop presence starts it again. Users can choose always-warm operation when eliminating cold starts matters more than compute cost.

Bundled PostgreSQL Is Allowed

A user-dedicated plugin may run its application and private PostgreSQL as one stateful container appliance. The database is never shared or publicly exposed.

# Entrypoint starts private PostgreSQL first.
export PGDATA=/data/postgres
initdb -D "$PGDATA" --username=plugin
postgres -D "$PGDATA" -h 127.0.0.1 &

# The app connects over loopback.
export DATABASE_URL=postgresql://plugin@127.0.0.1/plugin
exec node server.mjs

# Manifest requirements:
placement: "either"
isolation: "user-dedicated"
storage.persistent: true

Persistent and private

PGDATA lives under /data on one user/plugin-specific single-writer volume. Pod replacement or suspension does not delete it.

One replica

Bundled PostgreSQL is for user-dedicated stateful workloads only. It listens on loopback, runs non-root, and is never horizontally autoscaled.

Cloud stays cloud

Cloud-agent events use the cloud runtime and its private volume.

Local stays local

Local-agent events use the desktop runtime and its private volume. OpenLeash does not copy or merge database files.

Execution Follows The Agent

Persistence does not make two databases synchronizable. Use conversation context for consistent history-aware behavior.

edgeLocal agents

Run locally; /data is private to that desktop runtime.

serverCloud agents

Run in cloud; /data is private to that cloud runtime.

eitherLocal and cloud agents

Run where the event originates; use conversation context for shared history.

Publish Only After It Passes

Keep the digest out during local iteration. Pin the pushed digest before submitting the release so reviewed source cannot be replaced.

npm install
npm run smoke
docker push ghcr.io/acme/history-aware:1.0.0
docker inspect --format='{{index .RepoDigests 0}}' \
  ghcr.io/acme/history-aware:1.0.0
Publish a versioned OCI image and immutable digest
Declare only required events, effects, permissions, resources, placement, storage, timeout, and failure mode
Verify signature, timestamp, protocol, plugin identity, version, and requestId
Use conversation context—not a private database—as the default history source
Make writes idempotent because a signed request can be retried
Apply schema migrations before accepting traffic
Test SIGTERM shutdown, database reconnect, health failures, and container replacement
Never trust a caller-provided user id as the only database isolation mechanism