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_LEVEL will be set to "info" if not otherwise defined in the workspace.
  • If DATABASE_URL is not defined in the workspace, it will be treated as a required variable because its default value is null.

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.
  • install runs inside a disposable container with the agent mounts to prepare dependencies.
  • postinstall runs 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 shell first
  • Use cli for 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 under alias (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
    }
});