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
// 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
// Execution modes
"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
// Environment configuration
"env": {
"LOG_LEVEL": "info",
"DATABASE_URL": null
},
// Auto-configuration (optional)
"enable": ["other-agent"], // Auto-enable other agents
"repos": { // Auto-add repositories
"repo1": "https://github.com/org/repo.git"
}
}
Field Descriptions
| Field | Required | Description |
|---|---|---|
container |
Yes | Base container image from Docker Hub or other registry |
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 |
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. |
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.
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:
preinstallruns on the host before the agent is added to the workspace.installruns inside a disposable container with the agent mounts to prepare dependencies.postinstallruns inside the newly started agent container and triggers a restart when it finishes.
# manifest.json
"preinstall": [
"npm run prepare-assets"
],
"install": "npm install express body-parser",
"postinstall": "npm run seed"
# 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 sh -lc "cd '$PWD' && npm run seed"
docker restart ploinky_myrepo_MyAgent
3. Enablement
# Register agent in workspace
enable agent MyAgent
# Creates entry in .ploinky/agents
{
"ploinky_project_abc123_agent_MyAgent": {
"agentName": "MyAgent",
"containerImage": "node:18-alpine",
"createdAt": "2024-01-01T00:00:00Z",
...
}
}
4. Startup
# Start all enabled agents
start
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
- 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=/workspace # Mounted workspace
CODE_PATH=/code # Agent code directory
PORT=7000 # Default service port
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/mcps/MyAgent/status
http://localhost:8088/mcps/MyAgent/process
# From client
client status MyAgent
client task 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 /workspace 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
- 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
}
}
}
Scripts must live in the agent root (mounted as /code) and simply return exit code 0 for success. interval controls how often the probe runs (seconds), timeout caps each execution, and the success/failure thresholds set how many consecutive results are required before the CLI reports a pass or restart-worthy failure. Missing probes are treated as healthy by default; repeated liveness 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), while readiness failures currently emit a warning (Container failed to become ready) without killing the process.
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
}
});