Agent Specification
Complete guide to creating, configuring, and deploying Ploinky agents.
Manifest Structure
Every agent is defined by a manifest.json file that specifies its container, dependencies, and behavior:
Complete Manifest Schema
{
// Required fields
"container": "node:18-alpine", // Docker/Podman image (alias: "image")
// Lifecycle commands
"preinstall": "scripts/bootstrap.sh", // Host command before the agent is registered
"install": "npm install", // Run inside the container before first start
"postinstall": "npm run seed", // Run after the container starts, then restarts it
"update": "npm update", // Run when agent needs updating
// Host hooks (run on the host machine, not inside the container)
"hosthook_aftercreation": "echo created", // Host command after container creation
"hosthook_postinstall": "echo done", // Host command after postinstall completes
// Execution modes
"start": "node bootstrap.js", // Runs before the agent command
"cli": "node repl.js", // Interactive CLI command (cli)
"agent": "node server.js", // Long-running service (start)
// Metadata
"about": "Express API server", // Description shown in listings
"endpoints": { // OpenAI-compatible endpoints
"chatCompletions": {
"command": "node",
"args": ["/code/openai-chat.js"],
"supportsStream": true
},
"models": {
"command": "node",
"args": ["/code/openai-models.js"]
}
},
"capabilities": { // Optional open-ended capability metadata
"tags": ["coding-agent"],
"summary": "Short agent summary",
"whenToUse": "Use for code-focused requests.",
"input": {
"conventions": "Natural language prompt; supports OpenAI chat payloads.",
"schema": {
"type": "object",
"properties": {
"messages": { "type": "array" },
"stream": { "type": "boolean" }
}
}
}
},
// Sandbox and runtime
"lite-sandbox": true, // Enable sandbox runtime auto-detection
"runtime": { // Optional runtime resources, not backend selection
"resources": {}
},
"containerSecurity": { // Allowlisted OCI policy; trusted manifest power
"privileged": true
},
"network": { // "default", "bridge", "host", or "none"
"mode": "default"
},
// Readiness checks
"readiness": { // Startup readiness protocol
"protocol": "tcp" // "tcp", "mcp", or "none"
},
// Router-owned HTTP surfaces; port is a private TCP target only
"httpServices": [
{
"slug": "dashboard",
"externalPrefix": "/dashboard",
"internalPrefix": "/",
"port": 3000,
"access": "authenticated"
}
],
"routerAccess": {
"workspaceLogs": false, // Exact private log-file operation capability
"httpRoutes": [
{ "path": "/assets/*", "access": "public" }
]
},
// SSO provider marker
"ssoProvider": true, // Marks this agent as an SSO provider
// Environment configuration
"env": {
"LOG_LEVEL": "info",
"DATABASE_URL": null
},
// Per-profile configuration
"profiles": { // Per-profile overrides
"production": {
"env": { "LOG_LEVEL": "warn" },
"install": "npm ci",
"secrets": ["API_KEY"],
"openPorts": ["127.0.0.1:9090:9090/tcp"], // Private inner support mapping; never physical-host publication
"configProviders": ["config-provider global"]
}
},
// Optional generic startup config provider contract
"providesConfig": {
"command": "node runtime/provider.mjs",
"outputs": [
{ "name": "SERVICE_REGION", "sensitive": false }
]
},
// Volumes and mounts
"volumes": { // Extra bind mounts
"data": "/app/data" // hostRelPath: containerPath
},
"volumeOptions": { // Per-volume permissions
"/app/data": { "chmod": 493, "readOnly": true }
},
// Directives
"ploinky": ["pwd enable", "sso enable"], // Ploinky directives (string or array)
// Auto-configuration (optional)
"enable": ["other-agent"], // Auto-enable other agents
"repos": { // Auto-install repositories
"repo1": "https://github.com/org/repo.git"
}
}
httpServices; routed additional servers follow /base-agent-additional-server/<agent>/<port>/<suffix> and HttpRouteAccessPolicy. Do not add edgePorts, outer/physical publication, UDP, Cloudflare, tunnel, DNS, topology, consumer-binding, or generic server-inventory fields. openPorts is inner-runtime metadata only. No manifest field can change the Box's two fixed outer mappings or publish private Router 8081.
Field Descriptions
| Field | Required | Description |
|---|---|---|
container / image |
Yes | Base container image from Docker Hub or other registry. Both field names are accepted. |
preinstall |
No | Host-side command executed before the agent is registered. Accepts either a string or an array of commands. |
install |
No | One-time setup command that runs inside a disposable container before the main agent container starts. |
postinstall |
No | Command (string or array) executed inside the running container immediately after startup; the container restarts once the hook completes. |
update |
No | Command to update agent dependencies |
cli |
No | Interactive command for ploinky cli (runs inside the agent container). When omitted, Ploinky now falls back to /Agent/default_cli.sh, a safe helper that exposes basic inspection commands such as whoami, pwd, ls, env, date, and uname. |
agent |
No | Service command for ploinky start |
about |
No | Human-readable description |
endpoints |
No | Endpoint configuration for /v1/chat/completions, /v1/models, and /agent-card. A missing endpoints.models handler produces one fallback model using top-level capabilities.tags, or generic-agent when no tags are declared. |
capabilities |
No | Open-ended agent capability metadata. Normalized tags become the fallback model's functional tags when no custom models handler exists. |
env |
No | Defines environment variables. Can be an array of required variable names or an object to specify default values. See details below. |
enable |
No | Agents to auto-enable when this agent is enabled. Supports global/devel scopes and optional as <alias> to register duplicate instances under unique container names. See details in the Advanced Features section. |
repos |
No | Repositories to auto-add when this agent is enabled |
volumes |
No | Map of additional host paths to mount inside the container. Keys are host paths (absolute or relative to the workspace root), values are container destinations. Ploinky creates missing host directories and adds -v hostPath:containerPath when launching the container. |
volumeOptions |
No | Per-volume behavior keyed by container destination. Numeric chmod applies to the host path; readOnly: true is enforced by Docker, Podman, bwrap, and Seatbelt. |
start |
No | Command that runs before the agent command during startup (e.g., bootstrap scripts). |
hosthook_aftercreation |
No | Host-side command executed after the container is created. |
hosthook_postinstall |
No | Host-side command executed after postinstall completes inside the container. |
lite-sandbox |
No | Boolean. Outside a marked box, enables host sandbox runtime auto-detection for the whole agent process (selects bwrap on Linux or seatbelt on macOS). Inside a marked box the box marker wins: every Ploinky-managed agent, helper, sidecar, probe, and install-container path uses nested Podman, with no Docker, bwrap, or Seatbelt fallback. For one-off sandboxed jobs from inside a container, use the Basic catalog bwrap-runner agent's sandbox_exec tool instead; it does not change lite-sandbox dispatch. |
runtime |
No | Object for runtime resources such as declarative env and storage. String backend selectors such as "bwrap" or "seatbelt" are no longer supported; use lite-sandbox plus ploinky sandbox disable when container testing is needed. |
containerSecurity |
No | Allowlisted whole-container OCI security policy. The supported field is privileged: true; unknown raw runtime flags are rejected. This is trusted manifest power. |
network |
No | Selects exactly default, bridge, host, or none. Host mode still requires an exact Ploinky generation capability; a manifest request cannot grant it. Managed bridge launches use the fixed hosts-file/host-gateway transport contract, which is not authorization. |
readiness |
No | Object that configures startup readiness checks. An explicit protocol may be tcp, mcp, or none. Use none only for a true worker with no serving readiness surface. |
httpServices |
No | Declares Router-owned browser/protocol services with validated slug, prefixes, access policy, and optional integer port. Omitted port uses the agent's primary private target; each distinct explicit port creates or reuses one private TCP mapping. |
routerAccess.httpRoutes |
No | Declares agent-relative Router paths with public, guest, or authenticated access. Declarations are expanded under the effective route key and cannot claim Router control paths. |
routerAccess.workspaceLogs |
No | Boolean capability for the generation-bound private Router/Policy log-file operation. It does not publish a route or grant arbitrary filesystem access. |
providesConfig / configProviders |
No | Declares or selects generic host-side startup config providers. Outputs are allowlisted and persisted by Ploinky before final-graph enablement; providers cannot own edge publication, topology, or Router credentials. |
ssoProvider |
No | Boolean marker for agents that implement the workspace SSO provider runtime interface. |
profiles |
No | Object of per-profile configurations. Profile blocks can override lifecycle/env/mount policy and can select network, containerSecurity, openPorts, configProviders, and dependency enable entries. openPorts remains private inner-runtime metadata and cannot alter the outer box. |
ploinky |
No | Ploinky directives as a string or array of strings (e.g., "pwd enable", "sso enable"). |
The env Property
The env property is a flexible way to declare an agent's required environment variables and provide defaults.
1. Array of Strings (Required Variables)
To declare that an agent requires certain variables to be set in the workspace (e.g., via ploinky var ...), provide an array of names. If a variable is not set, Ploinky will throw an error on start.
"env": ["API_KEY", "DATABASE_URL"]
2. Object (Default Values)
To provide default values, use an object where the key is the environment variable name.
"env": {
"LOG_LEVEL": "info",
"API_PORT": 8080,
"DATABASE_URL": null
}
LOG_LEVELwill be set to"info"if not otherwise defined in the workspace.- If
DATABASE_URLis not defined in the workspace, it will be treated as a required variable because its default value isnull.
3. Generated Agent Secrets
For workspace-owned secrets that belong to one agent, set generatedSecret: true. Ploinky derives the value from PLOINKY_DERIVED_MASTER_KEY, the current repo name, the current agent name, and the env name, and ignores operator-provided values with the same name.
"env": [
{
"name": "AGENT_ENCRYPTION_KEY",
"required": true,
"generatedSecret": true
}
]
4. Host-hook-only Values
An object-form entry may set runtime: false. Ploinky resolves and validates the value for host lifecycle hooks, startup config providers, image templating, and restart hashing, but omits the value and its provenance marker from container metadata and sandbox process environments. The exclusion dominates a duplicate expose entry with the same name, so expose cannot reintroduce the value at runtime. This is intended for a host hook that materializes a generated, read-only runtime input.
"env": [
{
"name": "CONFIG_GENERATION_SECRET",
"sharedGeneratedSecret": true,
"runtime": false
}
]
Agent Lifecycle
1. Creation
# Create new agent
new agent myrepo MyAgent node:20
# Creates:
.ploinky/repos/myrepo/MyAgent/
├── manifest.json
└── (agent files)
2. Installation
When an agent is first enabled, Ploinky evaluates lifecycle hooks in this order:
preinstall— runs on the host before the agent is added to the workspace.hosthook_aftercreation— runs on the host after the container is created.install— runs before runtime launch to prepare dependencies. Container agents use a disposable install container; host-sandbox agents use the host dependency cache.postinstall— runs inside the running container and triggers a restart when it finishes.hosthook_postinstall— runs on the host after postinstall completes.
# manifest.json
"preinstall": [
"npm run prepare-assets"
],
"hosthook_aftercreation": "echo 'container created'",
"install": "npm install express body-parser",
"postinstall": "npm run seed",
"hosthook_postinstall": "echo 'all hooks done'"
# install executes in a disposable container
docker run -v $PWD:$PWD node:18-alpine sh -c "npm install express body-parser"
# postinstall executes inside the running agent container, then restarts it
docker exec ploinky_myrepo_MyAgent_project_a1b2c3 sh -lc "cd '$PWD' && npm run seed"
docker restart ploinky_myrepo_MyAgent_project_a1b2c3
3. Enablement
# Register agent in workspace
enable agent MyAgent
# Creates entry in .ploinky/agents.json
# Container naming pattern: ploinky_<repo>_<agent>_<project>_<cwdHash>
{
"ploinky_myrepo_MyAgent_project_a1b2c3": {
"agentName": "MyAgent",
"containerImage": "node:18-alpine",
"createdAt": "2024-01-01T00:00:00Z",
...
}
}
4. Startup
# Start all enabled agents
start
Startup first prepares the recursive manifest repository set and planning graph without launching agent processes, then captures an early inactive generation with exact graph identities and every retained route targetless. The fatal static preinstall hook and startup config providers run against that topology. Ploinky then aborts the early lease, reloads the registry, re-evaluates retained predecessor runtime hashes, rotates newly stale tuples, and captures the final inactive targetless generation. Only its exact lease may authorize targets when blocking dependency waves start. Each serving target is added only by a coordinated route-and-policy apply for that wave. Enabled agents outside the final graph start after those waves, and dependencies selected by a no-wait edge are launched last without a readiness wait.
5. Runtime
During runtime, agents can be in different states:
- Running: Container active, service responding
- Stopped: Container exists but not running
- Exited: Container terminated (check exit code)
- Removed: Container deleted
Command Types
CLI Command
Interactive command for direct user interaction:
# Usage
cli MyAgent
You can define the CLI in two equivalent ways:
{
"cli": "python -i"
}
{
"commands": {
"cli": "python -i"
}
}
The commands block lets you group related entries (for example commands.cli alongside commands.run). If neither cli nor commands.cli is present, Ploinky falls back to /Agent/default_cli.sh.
Agent Command
Long-running service for API endpoints:
# manifest.json
"agent": "node server.js"
# server.js
const express = require('express');
app.get('/mcp/status', (req, res) => {
res.json({ status: 'running' });
});
app.listen(7000);
Supervisor Mode
If no agent command is specified, Ploinky uses the default supervisor:
# /Agent/AgentServer.mjs provides:
- HTTP server on port 7000, mounted at /mcp
- MCP protocol version 2025-06-18
- Health check at /mcp/status
- Process management
- Automatic restarts
Environment Setup
Container Environment
Agents run with these environment variables:
AGENT_NAME=MyAgent # Agent name
AGENT_REPO=myrepo # Repository name
WORKSPACE_PATH=/root # Isolated container home backed by .data/<agent-or-alias>
CODE_PATH=/code # Agent code directory
PORT=7000 # Default service port
A private Router caller is bound to an exact launcher-reserved identity tuple: canonical identity agent:<repo>/<agent>, effective runtime instance id, and current enable generation. A route key or alias can select that record but never replaces the canonical identity. Re-enable, instance replacement, or rebinding creates a new tuple; a stable agent secret or stale alias cannot authorize the old tuple. Private assertions are request-bound caller proofs that still require effective policy admission and an exact caller ACL; they are not user or administrator credentials.
PLOINKY_AGENT_ID=agent:myrepo/MyAgent
PLOINKY_AGENT_INSTANCE_ID=<effective-runtime-instance-id>
PLOINKY_AGENT_ENABLE_GENERATION=<current-enable-generation>
Volume Mounts
| Host Path | Container Path | Purpose |
|---|---|---|
$(pwd) |
$(pwd) |
Workspace access |
/Agent |
/Agent |
Supervisor runtime |
.ploinky/repos/X/Y |
/code |
Agent code |
Exposing Variables
# Set variable in workspace
ploinky var DATABASE_URL postgres://localhost/mydb
# Expose to agent
ploinky expose DATABASE_URL $DATABASE_URL MyAgent
# Agent can now access:
process.env.DATABASE_URL
API Development
Basic HTTP Server
// server.js
const http = require('http');
if (req.url === '/mcp/status') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
} else if (req.url.startsWith('/mcp/')) {
// Handle API routes
const path = req.url.substring(5);
res.writeHead(200);
res.end(`API path: ${path}`);
} else {
res.writeHead(404);
res.end('Not found');
}
});
server.listen(7000, () => {
console.log('Agent server running on port 7000');
});
Express.js API
// api.js
const express = require('express');
const app = express();
app.use(express.json());
app.get('/mcp/status', (req, res) => {
res.json({
status: 'healthy',
agent: process.env.AGENT_NAME,
uptime: process.uptime()
});
});
// Custom endpoints
app.post('/mcp/process', (req, res) => {
const { data } = req.body;
// Process data
res.json({
result: `Processed: ${data}`,
timestamp: new Date()
});
});
app.listen(7000);
Python Flask API
# api.py
from flask import Flask, jsonify, request
import os
app = Flask(__name__)
@app.route('/mcp/status')
def status():
return jsonify({
'status': 'healthy',
'agent': os.environ.get('AGENT_NAME'),
'language': 'python'
})
@app.route('/mcp/process', methods=['POST'])
def process():
data = request.json
return jsonify({
'result': f"Processed: {data}",
'method': 'python'
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7000)
Accessing Your API
Once deployed, access your agent's API through the routing server:
# Local development
http://localhost:8088/MyAgent/mcp/status
http://localhost:8088/MyAgent/mcp/process
# From client
client status MyAgent
client tool health.check --agent MyAgent
Example Agents
Simple Shell Agent
{
"container": "alpine:latest",
"install": "apk add curl jq",
"cli": "/bin/sh",
"about": "Alpine Linux shell with curl and jq"
}
Tip: If you omit the cli field entirely, Ploinky will attach the bundled /Agent/default_cli.sh script so you still have access to safe inspection commands via ploinky cli <agent> <command>. Launching ploinky cli <agent> with no arguments drops you into an interactive prompt; type help to see the allowed commands and exit when you are finished.
Node.js Development Agent
{
"container": "node:20",
"install": "npm install -g nodemon typescript @types/node",
"update": "npm update -g",
"cli": "node",
"agent": "nodemon --watch /code server.js",
"about": "Node.js development environment with hot reload"
}
Python AI Assistant
{
"container": "python:3.11",
"install": "pip install openai numpy pandas flask",
"update": "pip install --upgrade openai",
"cli": "python -i",
"agent": "python api_server.py",
"env": ["OPENAI_API_KEY"],
"about": "Python AI assistant with OpenAI integration"
}
Database Client Agent
{
"container": "postgres:15",
"install": "echo 'PostgreSQL client ready'",
"cli": "psql -U postgres",
"env": ["POSTGRES_PASSWORD"],
"about": "PostgreSQL client for database operations"
}
Multi-Agent System
{
"container": "node:18-alpine",
"install": "npm install",
"agent": "node orchestrator.js",
"about": "Orchestrator agent",
"enable": ["worker1", "worker2", "database"],
"repos": {
"workers": "https://github.com/myorg/worker-agents.git"
}
}
Best Practices
Container Selection
- Use Alpine-based images for smaller size
- Pin specific versions (node:18.19.0 vs node:18)
- Consider multi-stage builds for complex agents
- Minimize layers in install commands
Security
- Never hardcode secrets in manifest.json
- Use environment variables for sensitive data, and
generatedSecret: truefor agent-owned generated secrets - Run processes as non-root user when possible
- Validate all input in API endpoints
Performance
- Keep install commands minimal
- Cache dependencies in agent directory
- Use health checks for monitoring
- Implement graceful shutdown handlers
Development
- Test locally with
shellfirst - Use
clifor interactive debugging - Check logs with container runtime directly
- Version control your agent code separately
Troubleshooting
Common Issues
| Problem | Cause | Solution |
|---|---|---|
| Container exits immediately | No long-running process | Add agent command or use supervisor |
| Port 7000 not accessible | Service not binding correctly | Bind to 0.0.0.0:7000, not localhost |
| Install command fails | Missing dependencies in base image | Use fuller base image or add apt/apk commands |
| Environment variables not set | Not exposed to agent | Use expose command |
| API returns 404 | Routing misconfiguration | Check path starts with /mcp/ |
Debugging Commands
# Check agent status
status
Health Checks
Implement health endpoints for monitoring:
// Health check endpoint
app.get('/mcp/status', (req, res) => {
const health = {
status: 'healthy',
checks: {
database: checkDatabase(),
memory: process.memoryUsage(),
uptime: process.uptime()
}
};
const isHealthy = Object.values(health.checks)
.every(check => check !== false);
res.status(isHealthy ? 200 : 503).json(health);
});
Manifest-driven probes
Ploinky now reads an optional health object from each agent manifest so containers can define their own liveness/readiness probes without a cluster:
{
"container": "node:20",
"agent": "node server.js",
"health": {
"liveness": {
"script": "liveness_probe.sh",
"interval": 2,
"timeout": 5,
"failureThreshold": 5,
"successThreshold": 1
},
"readiness": {
"script": "readiness_probe.sh",
"timeout": 5,
"failureThreshold": 5,
"continuous": false
}
}
}
Scripts must live in the agent root (mounted as /code) and return exit code 0 for success. interval controls how often the probe runs (seconds), timeout caps each execution, and the thresholds set how many consecutive results are required. During startup an explicit manifest readiness.protocol of tcp, mcp, or none wins. Without an explicit protocol, a start-only service with health.readiness.script uses that script as its blocking startup probe. A configured but missing script, execution error, or exhausted failure threshold fails the dependency wave and prevents its dependents from starting. Later watchdog reuse of readiness is fail-closed by default: an exhausted recurring readiness or liveness probe inactivates routing and schedules managed recovery. Setting readiness continuous: false keeps an expensive full readiness attestation activation-only and requires a separate recurring liveness script. Every probe has an in-container hard deadline, runs in an exact process session inside an init-reaped managed container, and fails closed when process-tree cleanup cannot be proved. Repeated health failures trigger automatic container restarts that follow a CrashLoopBackOff curve (base 10s delay, doubling up to five minutes, reset after 10 minutes of stable uptime or any manual stop/restart/refresh).
Advanced Features
Auto-Configuration
Agents can automatically configure their environment by specifying repositories to add and other agents to enable.
The enable property
The enable property is an array of strings that specifies which other agents should be automatically enabled when this agent is enabled. It supports different scopes for finding the agent and optional aliases to keep containers distinct:
"agentName": Enables an agent from the same repository. This is the default behavior."agentName global": Enables an agent from the global repository."agentName devel repoName": Enables an agent from the specified repository (repoName) in development mode."agentName ... as alias": Adds an alias so the resulting container is recorded underalias(required when the same agent is enabled more than once).
Aliases behave exactly like CLI-provided aliases: they must be unique per workspace, become the canonical container names for future commands (refresh agent, disable agent, etc.), and trigger an alias already exists error if reused.
# manifest.json
{
"container": "node:18",
"agent": "node server.js",
"enable": [
"database", // Enable 'database' from the current repo
"cache global", // Enable 'cache' from the global repo
"logger devel utils", // Enable 'logger' from the 'utils' repo in devel mode
"explorer as explorer2" // Enable an 'explorer' instance with alias explorer2
],
"repos": {
"utils": "https://github.com/org/utils.git"
}
}
# When this agent is enabled:
1. Adds the 'utils' repository.
2. Enables the 'database' agent from the current agent's repository.
3. Enables the 'cache' agent from the global repository.
4. Enables the 'logger' agent from the 'utils' repository in development mode.
5. Enables another 'explorer' container registered under the alias explorer2 (use the alias for future CLI operations).
Custom Supervisor
Override the default supervisor with custom logic:
// custom-supervisor.js
const { spawn } = require('child_process');
const http = require('http');
// Start main process
const main = spawn('node', ['app.js']);
// Health check server
http.createServer((req, res) => {
if (req.url === '/mcp/status') {
res.writeHead(200);
res.end(JSON.stringify({
status: main.exitCode === null ? 'running' : 'stopped',
pid: main.pid
}));
}
}).listen(7000);
// Restart on crash
main.on('exit', (code) => {
if (code !== 0) {
console.log('Restarting after crash...');
// Restart logic
}
});