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:

Typical CLI-focused manifest

{
  "container": "docker.io/assistos/ploinky-node:24-bookworm-tools",
  "install": "sh /code/scripts/installPrerequisites.sh",
  "env": [
    "WORKSPACE_PATH",
    "ACHILLES_MODEL_PLAN",
    "ACHILLES_MODEL_CODE",
    "ACHILLES_DEBUG"
  ],
  "enable": ["proxies/soul-gateway no-wait"],
  "webchat": { "forwardEnvelope": true },
  "cli": "node /code/src/cli.mjs",
  "about": "CLI agent that manages, generates, and tests skill definitions."
}

This configuration represents the usual shape of a CLI-only agent manifest: one pinned runtime image, a prerequisite command, declared inputs, optional background dependencies, an interactive CLI, and no exposed agent port. Because it defines neither start nor agent, Ploinky starts its default AgentServer to provide the private MCP surface. webchat.forwardEnvelope opts the CLI into WebChat's structured input envelope; it is not required for normal newline-terminated terminal input.

Slim edge boundary: Active manifests must not use 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 Description
container / image Base container image from Docker Hub or other registry. Both field names are accepted.
preinstall Host-side command executed during coordinated startup after topology generation and before startup config providers. The value is one script path or inline command string.
install One-time setup command that runs inside a disposable container before the main agent container starts.
postinstall Command string executed inside the running container immediately after startup; the container restarts once the hook completes.
update Command to update agent dependencies
cli 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 Service command for ploinky start
about Human-readable description
endpoints 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 Open-ended agent capability metadata. Normalized tags become the fallback model's functional tags when no custom models handler exists.
env Defines environment inputs. A string array imports available values without making them required; object entries can declare defaults, source names, generation, runtime delivery, and required: true. See details below.
enable Agent dependencies added to the startup graph when this agent is started. Supports global/devel scopes, no-wait, and optional as <alias> registration. See the Advanced Features section.
repos Repositories that startup installs or resolves before it constructs the agent dependency graph.
volumes 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 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 Main container entry command. When agent or commands.run is also declared, Ploinky runs that agent command as a detached sidecar.
hosthook_aftercreation Host-side command executed after the container is created.
hosthook_postinstall Host-side command executed after postinstall completes inside the container.
lite-sandbox 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 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 Root-only allowlisted OCI security policy. The boolean fields privileged and nestedPodman default to false and are mutually exclusive. Unknown fields and raw runtime flags are rejected. This is trusted manifest power.
network 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 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.
routerAccess.httpRoutes 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 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 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 Boolean marker for agents that implement the workspace SSO provider runtime interface.
hardwareLimits The agent's own memory, cpus and pidsLimit limits, for every agent. Accepted at the manifest root and in a profile; see the hardwareLimits property.
profiles Object of per-profile configurations. Profile blocks can override supported lifecycle, environment, mount, network, openPorts, configProviders, hardwareLimits, and dependency enable entries. containerSecurity remains root-only, and openPorts cannot alter the outer Box.
ploinky Ploinky directives as a string or array of strings (e.g., "pwd enable", "sso enable").

The containerSecurity Property

Set nestedPodman: true only when an agent's OCI container must launch Podman inside a Ploinky Box. The setting is accepted only at the manifest root, only for the container runtime, and only when Ploinky is running inside a marked Box. Ploinky rejects it for Bubblewrap, Seatbelt, and containers outside the Box, and rejects a manifest that also sets privileged: true.

{
  "containerSecurity": {
    "nestedPodman": true
  }
}

For an admitted manifest, Ploinky adds only SYS_ADMIN and NET_ADMIN, passes through /dev/fuse and /dev/net/tun, applies label=disable, and selects the fixed /opt/ploinky/ploinky-box/seccomp/podman-nested-pid-fallback.json seccomp profile. Manifests cannot replace or extend this fixed argument set through direct capability fields or llmRuntime.runtimePolicy.

The hardwareLimits Property

Any agent, with or without the LLM runtime, declares its CPU, RAM and process-count limits in a top-level hardwareLimits object at the manifest root or in a profile. Only memory, cpus and pidsLimit are accepted, each validated exactly as before; other keys are refused, and GPU shares remain administrator-only in Explorer Settings → Hardware limits.

{
  "hardwareLimits": { "memory": "512m", "cpus": "0.5", "pidsLimit": 128 },
  "profiles": {
    "default": {},
    "dev": { "hardwareLimits": { "cpus": "1" } }
  }
}

The manifest root and the resolved profile are separate layers. The selected profile overrides the default profile key by key and inherits the keys it does not declare. The order is unchanged: built-in defaults, manifest, LLM catalog, profile, then the administrator's stored limits, which override every declaration. Inside a Box, a declared limit is enforced only when hardware limits are on and the Box is prepared; otherwise, and in Bubblewrap or Seatbelt, the agent is refused with a reason and fix instead of running unlimited.

The same keys under llmRuntime.runtimePolicy.resources still work but are deprecated: each command or Router start prints one warning per agent naming the deprecated paths and hardwareLimits. A key declared in both places of the same manifest root or profile with different values refuses the agent with a declaration_conflict fix that names the manifest root or the profile; equal values are accepted, compared by meaning (1g equals 1024m, 1.0 equals 1). Moving a declaration to hardwareLimits keeps the same limits hash, so a running agent is not restarted; the rendered arguments follow the declaration's own spelling (--memory 1g versus --memory 1024m). shmSize, ulimits, devices, ipc and the other llmRuntime.runtimePolicy settings are not deprecated.

Upgrade note: earlier releases dropped a selected non-default profile's llmRuntime settings, so limits declared under llmRuntime.runtimePolicy.resources in such a profile were silently ignored. They now apply: on the next start or Apply the agent can be recreated with the new limits, or refused with a fix where limits cannot be enforced (hardware limits off or an unprepared Box).

The env Property

The env property declares the environment inputs an agent may receive, including optional imports, explicit requirements, defaults, and generated values.

1. Array of Strings (Optional Imports)

An array of names imports values that are available from the workspace variable sources. A missing value does not fail startup. Use an object entry with required: true when startup must stop until the operator supplies the value.

"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.
  • DATABASE_URL has no usable default and remains optional unless the entry uses object form with required: true.
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:

  1. preinstall — runs on the host before the agent is added to the workspace.
  2. hosthook_aftercreation — runs on the host after the container is created.
  3. install — runs before runtime launch to prepare dependencies. Container agents use a disposable install container; host-sandbox agents use the host dependency cache.
  4. postinstall — runs inside the running container and triggers a restart when it finishes.
  5. 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

Enablement, disablement, and manual startup

enable agent <agent-or-repo/agent> is an activation command, not only a registry edit. Ploinky validates the manifest and runtime policy, creates the agent work directory and source links, records a new instance and enable generation, creates the runtime, waits for readiness, and publishes its Router route only after the runtime is ready. Enablement does not replace the workspace's primary/static agent selected by start.

# Register and start an isolated instance (the default mode)
enable agent tools/skill-cli

# Register and start another independent instance
enable agent tools/skill-cli as review-cli

# Remove the route, registry record, and exact runtime; persistent .data remains
disable agent review-cli

disable agent <alias|repo/agent> first removes the target from the active route generation, then stops and removes its exact container or sandbox process and deletes its enabled-agent record. The source repository and persistent .data/<agent-or-alias> directory remain available for a later enablement. An unqualified name is rejected when it could refer to multiple enabled agents; use the alias or a repository-qualified name.

Manual startup policy

{
  "startup": "manual"
}

Omitting startup means automatic. The manual policy applies only to an enabled agent outside the static agent's dependency graph. A normal workspace start leaves a stopped manual agent stopped and removes its stale route; it retains a manual agent that is already running. A static agent and an explicit dependency always start even when their manifest says manual. To activate a stopped manual instance explicitly, use restart <agent-or-alias>; Ploinky recreates or starts the runtime, checks readiness, then restores its route.

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);

Default AgentServer

When the manifest defines none of start, agent, or commands.run, Ploinky starts sh /Agent/server/AgentServer.sh. It serves the private MCP endpoint from the configured agent port and loads mcp-config.json. See MCP configuration for the configuration format and Router access rules.

Environment Setup

Container Environment

Ploinky combines the selected manifest/profile environment with runtime-owned variables. Manifest values resolve from workspace secrets, process environment, .env, and declared defaults; runtime-owned names are added after that resolution and cannot be overridden by a manifest, profile, or exposed secret.

VariableRuntime value
AGENT_NAMEThe manifest agent name.
WORKSPACE_PATH/root for an isolated instance; the selected workspace or development-repository path for global and development modes.
PLOINKY_WORKSPACE_ROOTThe canonical workspace root.
HOME/root, backed by the instance's persistent data directory.
PLOINKY_MCP_CONFIG_PATH/code/mcp-config.json, the staged MCP configuration path.
NODE_PATH/code/node_modules for the default AgentServer and agent code.
PLOINKY_AGENT_ID, PLOINKY_AGENT_PRINCIPAL, PLOINKY_AGENT_INSTANCE_ID, and PLOINKY_AGENT_ENABLE_GENERATIONGenerated identity values that bind the process to one canonical agent, instance, and enable generation.

For managed routable runtime modes, Ploinky also supplies validated Router discovery and per-agent request-signing material after network attestation. It never injects the workspace master key, another agent's credentials, or reusable Router credentials into an agent with network.mode: "none". Agents must treat every PLOINKY_* identity, Router, and credential variable as runtime-owned.

Volume Mounts

Enable modeProject mount and working pathPersistent home
isolated (default).data/<agent-or-alias> → /root; WORKSPACE_PATH=/rootThe same .data/<agent-or-alias> directory is mounted read-write at /root.
globalWorkspace root → its selected project path.data/<agent-or-alias> → /root read-write.
devel <repo>.ploinky/repos/<repo> → that development-repository path.data/<agent-or-alias> → /root read-write.
Common mountContainer targetPurpose
Agent source tree/codeAgent code and mcp-config.json; mount mode follows the selected profile.
Ploinky Agent library/AgentRead-only default AgentServer and shared runtime helpers.
.ploinky/shared/sharedWorkspace-shared files.
Prepared dependency cache/code/node_modules and /Agent/node_modulesRead-only dependencies when the runtime uses the prepared cache.

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

MCP configuration

mcp-config.json defines the MCP tools, resources, and prompts served by the default AgentServer. Place the file beside the agent manifest. Ploinky synchronizes it into the instance work directory and sets PLOINKY_MCP_CONFIG_PATH=/code/mcp-config.json; the Router exposes the resulting MCP surface through /<agent-or-alias>/mcp without publishing the agent's private listener.

The default server is started only when the manifest has no start, agent, or commands.run command. An agent that supplies its own service command must start and maintain its own compatible MCP server if it needs an MCP surface.

{
  "tools": [
    {
      "name": "read_workspace_file",
      "title": "Read workspace file",
      "description": "Return an approved file from the agent workspace.",
      "command": "node",
      "args": ["tools/read-workspace-file.mjs"],
      "cwd": "workspace",
      "timeoutMs": 30000,
      "tags": ["authenticated"],
      "inputSchema": {
        "path": { "type": "string", "description": "Workspace-relative file path" }
      }
    }
  ],
  "prompts": [
    {
      "name": "review_change",
      "description": "Prepare a change-review request.",
      "messages": [{ "role": "user", "content": { "type": "text", "text": "Review the requested change." } }]
    }
  ]
}

For tools and resources, command, optional args, cwd, env, and timeoutMs define the child process. A command path containing a slash is resolved from /code; cwd: "workspace" selects the runtime working directory. The command receives a JSON payload on standard input. For a tool, it contains tool, input, and verified invocation metadata; standard output becomes the MCP text result, while a non-zero exit becomes an MCP error.

Tool tags establish the default Router policy: authenticated, admin, or internal. Missing tags mean authenticated. Unknown tags and the internal plus admin combination are denied. Router policy remains authoritative, so an agent configuration cannot bypass caller authentication, the active route generation, or a request-bound invocation token.

API Development

Basic HTTP Server

// server.js
const http = require('http');

const server = http.createServer((req, res) => {
    if (req.url === '/status') {
        res.writeHead(200, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ status: 'ok' }));
    } else if (req.url.startsWith('/api/')) {
        // 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('/status', (req, res) => {
    res.json({ 
        status: 'healthy',
        agent: process.env.AGENT_NAME,
        uptime: process.uptime()
    });
});

// Custom endpoints
app.post('/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('/status')
def status():
    return jsonify({
        'status': 'healthy',
        'agent': os.environ.get('AGENT_NAME'),
        'language': 'python'
    })

@app.route('/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://127.0.0.1:8080/MyAgent/status
http://127.0.0.1:8080/MyAgent/process

These examples are ordinary HTTP routes admitted through routerAccess.httpRoutes. The MCP endpoint at /MyAgent/mcp uses the MCP protocol and is not an alias for arbitrary HTTP handlers.

Example Agents

Create a CLI agent with MCP tools

Start with a directory in a repository that Ploinky discovers. Keep the manifest and MCP configuration at the agent root, and keep executable code below it:

PathRole
manifest.jsonDeclares the runtime, required environment names, dependencies, and interactive CLI.
mcp-config.jsonDeclares MCP tools, resources, and prompts for the default AgentServer.
src/cli.mjsImplements the interactive CLI selected by cli.
tools/read-workspace-file.mjsImplements a configured MCP tool and reads its JSON input from standard input.

Use the CLI-focused manifest shown in Manifest Structure as the starting point: it is the configuration shape used by the in-repository CLI agents—pinned Ploinky Node image, prerequisite script, explicit environment allow-list, optional enable dependency, webchat.forwardEnvelope, and a cli entry. Do not add start or agent when the bundled AgentServer should serve the configuration in mcp-config.json.

// tools/read-workspace-file.mjs
import fs from 'node:fs/promises';
import path from 'node:path';

let raw = '';
for await (const chunk of process.stdin) raw += chunk;
const { input = {} } = JSON.parse(raw || '{}');
const requested = String(input.path || '');
if (!requested || path.isAbsolute(requested) || requested.split('/').includes('..')) {
  throw new Error('path must be a workspace-relative path');
}
const target = path.join(process.cwd(), requested);
process.stdout.write(await fs.readFile(target, 'utf8'));

After the repository is available to the workspace, enable and use the agent by its repository-qualified reference. The alias is optional, but useful when more than one instance is needed:

enable agent <repository>/my-cli as review-cli
ploinky cli review-cli

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: true for 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 shell first
  • Use cli for interactive debugging
  • Inspect the selected agent with ploinky logs <agent>
  • 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 adds dependent agents to the coordinated startup graph when the owning agent starts. It supports different scopes for finding an agent and optional aliases to keep instances 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
    }
});