Ploinky Architecture

Technical architecture and implementation details of the Ploinky AI agent deployment system.

System Overview

Ploinky is built as a modular system with clear separation of concerns:

graph TD
    UI["User Interface
    CLI Commands / Web Console / Chat / Dashboard"]
    CLI["Ploinky CLI Core
    Command Handler / Service Manager / Config"]
    RS["Routing Server
    + Watchdog"]
    WS["Web Services
    WebTTY / Chat"]
    RT["Runtime Mgmt
    Container / Bwrap / Seatbelt"]
    AS["Agent Sandboxes
    Containers, bubblewrap processes, seatbelt jails"]

    UI --> CLI
    CLI --> RS
    CLI --> WS
    CLI --> RT
    RS --> AS
    WS --> AS
    RT --> AS
                        

Key Design Principles

  • Isolation First: Every agent runs in its own container
  • Workspace Scoped: All configuration is local to the project directory
  • Zero Global State: No system-wide installation or configuration
  • Git-Friendly: Configuration stored in .ploinky folder, can be gitignored
  • Runtime Agnostic: Supports Docker, Podman, bubblewrap (Linux), and seatbelt (macOS) transparently

Core Components

CLI Command System (cli/commands/cli.js)

The main entry point that handles all user commands:

// Command routing structure
handleCommand(args) {
    switch(command) {
        case 'add':      // Repository management
        case 'enable':   // Agent/repo activation
        case 'start':    // Workspace initialization
        case 'shell':    // Interactive container access
        case 'webchat':  // Web interface launchers
        // ... more commands
    }
}

Service Layer (cli/services/)

Service Responsibility
workspace.js Manages .ploinky directory and configuration
docker/ Container lifecycle management modules (runtime helpers, interactive commands, agent management)
repos.js Repository management and agent discovery
agents.js Agent registration and configuration
secretVars.js Environment variable and secrets management
config.js Global configuration constants
help.js Help system and documentation
bwrap/ Bubblewrap sandbox lifecycle management (Linux)
seatbelt/ Seatbelt sandbox lifecycle management (macOS)
agentRegistry.js Installed-agent lookup, principal resolution, runtime resources, and SSO-provider discovery
dependencyCache.js Stamp-based node_modules cache validation
dependencyInstaller.js npm install in containers with globalDeps merge
profileService.js Workspace profile management (dev/qa/prod)
workspaceDependencyGraph.js Agent dependency graph resolution

Container Management

Container Lifecycle

Ploinky manages containers with specific naming conventions and lifecycle hooks:

// Container naming convention
// Pattern: ploinky_<repo>_<agent>_<project>_<cwdHash>

Volume Mounts

Each container gets specific volume mounts for security:

{
    binds: [
        { source: process.cwd(), target: process.cwd() },      // Workspace
        { source: '/Agent', target: '/Agent' },                // Agent runtime
        { source: agentPath, target: '/code' }                 // Agent code
    ]
}

Runtime Detection

Automatically detects the runtime from the manifest and host:

// Runtime preference: podman > docker
// Default: podman/docker
// Host sandbox:
//   manifest "lite-sandbox": true β†’ macOS: seatbelt, Linux: bwrap
// Container override:
//   ploinky sandbox disable β†’ podman/docker even for lite-sandbox agents
// Legacy string selectors such as "runtime": "bwrap" are rejected.

Routing Server

Purpose

The RoutingServer (cli/server/RoutingServer.js) acts as a reverse proxy, routing API requests to appropriate agent containers:

// routing.json structure
{
    "port": 8088,
    "static": {
        "agent": "demo",
        "container": "ploinky_myproject_abc123_service_demo",
        "hostPath": "/path/to/demo/agent"
    },
    "routes": {
        "agent1": {
            "container": "ploinky_myproject_abc123_service_agent1",
            "hostPort": 7001
        },
        "agent2": {
            "container": "ploinky_myproject_abc123_service_agent2",
            "hostPort": 7002
        }
    }
}

Request Flow

  1. Client sends request to http://localhost:8088/apis/agent1/method
  2. RoutingServer extracts agent name from path
  3. Looks up agent's container port in routing.json
  4. Proxies request to http://localhost:7001/api/method
  5. Returns response to client

Static File Serving

The router serves static files from the host filesystem in two ways:

  • Static agent root (existing): requests like /index.html map to static.hostPath in routing.json.
  • Agent-specific static routing (new): requests like /demo/ui/index.html map to the hostPath of the demo agent from routes.demo.hostPath.
// Static agent root
GET /index.html            β†’ routing.static.hostPath/index.html
GET /assets/app.js         β†’ routing.static.hostPath/assets/app.js

// Agent-specific static routing
GET /demo/ui/index.html    β†’ routing.routes.demo.hostPath/ui/index.html
GET /simulator/app.js      β†’ routing.routes.simulator.hostPath/app.js

Blob Storage API

The router exposes a simple blob storage API for large files with streaming upload/download.

// Upload (streaming)
POST /blobs/<agentName>
Headers:
  Content-Type: application/octet-stream
  X-Mime-Type: text/plain   # optional; falls back to Content-Type
  X-File-Name: report.pdf   # optional; original filename for metadata
Body: raw bytes (streamed)

Response: 201 Created
{ "id": "", "url": "/blobs/<agentName>/", "size": N, "mime": "text/plain", "agent": "", "filename": "report.pdf" }

// Download (streaming, supports Range)
GET /blobs/<agentName>/<id>
HEAD /blobs/<agentName>/<id>
 - Streams bytes from <agentWorkspace>/blobs/<id> with metadata from .../blobs/<id>.json
 - Sets Content-Type, Content-Length, Accept-Ranges, and supports partial responses (206)

Watchdog Supervisor

The Watchdog (cli/server/Watchdog.js) supervises the RoutingServer process:

  • Circuit breaker: Max 5 restarts in 60 seconds; trips and halts on breach.
  • Exponential backoff: Initial 1s, 2x multiplier, max 30s between restarts.
  • Health checks: HTTP GET to /health every 30s, restart after 3 consecutive failures.
  • Container monitoring: Polls container state every 5 seconds.
  • Logging: Structured JSON to .ploinky/logs/watchdog.log.

Workspace System

Directory Structure

.ploinky/
β”œβ”€β”€ agents.json          # Enabled agents registry (+ _config key)
β”œβ”€β”€ .secrets             # Environment variables and secrets
β”œβ”€β”€ profile              # Active profile name (dev/qa/prod)
β”œβ”€β”€ ploinky_history      # CLI command history
β”œβ”€β”€ repos/               # Cloned agent repositories
β”‚   β”œβ”€β”€ basic/
β”‚   β”œβ”€β”€ demo/
β”‚   └── ...
β”œβ”€β”€ agents/              # Per-agent work directories
β”œβ”€β”€ code/                # Symlinks to agent code
β”œβ”€β”€ skills/              # Symlinks to agent skills
β”œβ”€β”€ logs/                # Router and watchdog logs
β”œβ”€β”€ shared/              # Shared data between agents
β”œβ”€β”€ running/             # PID files
β”œβ”€β”€ routing.json         # Live route table
β”œβ”€β”€ servers.json         # Web surface config (ports, tokens)
β”œβ”€β”€ transcripts/         # Conversation transcripts
└── deps/                # Dependency caches
    β”œβ”€β”€ global/          # Global node_modules per runtime key
    └── agents/          # Per-agent node_modules per runtime key

Agent Registry (agents/)

JSON file storing enabled agents and their configuration:

{
    "ploinky_project_abc123_agent_demo": {
        "agentName": "demo",
        "repoName": "demo",
        "containerImage": "node:18-alpine",
        "createdAt": "2024-01-01T00:00:00Z",
        "projectPath": "/home/user/project",
        "type": "agent",
        "config": {
            "binds": [...],
            "env": [...],
            "ports": [{"containerPort": 7000}]
        }
    }
}

Configuration Management

Workspace configuration persists across sessions:

// Stored in agents/_config
{
    "static": {
        "agent": "demo",
        "port": 8088
    }
}

Security Model

Container Isolation

  • Filesystem: Containers only access current workspace directory
  • Network: Isolated network namespace per container
  • Process: No access to host processes
  • Resources: Can set CPU/memory limits

Secret Management

Environment variables stored in .ploinky/.secrets with aliasing support:

API_KEY=sk-123456789
PROD_KEY=$API_KEY        # Alias reference
DATABASE_URL=postgres://localhost/db

Authentication Modes

Each agent can be independently configured with one of three auth modes via enable agent --auth none|pwd|sso:

  • none: No authentication (default).
  • pwd (local): Username/password auth with HMAC-signed JWT sessions. Cookie: ploinky_jwt. Session TTL: 4 hours.
  • sso (OIDC): Delegates to the configured SSO provider agent marked with "ssoProvider": true. Supports PKCE flow. Cookie: ploinky_sso.

Secure Wire Protocol

Agent-to-agent and browser-to-agent calls use HS256-signed invocation JWTs (60-second TTL) for request authentication. The shared secret (PLOINKY_WIRE_SECRET) is auto-generated per workspace. Replay protection is enforced via a memory cache.

Web Services Architecture

WebTTY/Console (cli/webtty/)

Provides terminal access through web browser:

// Component structure
server.js       // HTTP/WebSocket server
tty.js          // PTY management
console.js      // Client-side terminal UI
clientloader.js // Dynamic UI loader

WebChat (cli/webtty/chat.js)

Chat interface for CLI programs:

  • Captures stdout/stdin through PTY
  • WebSocket-based real-time communication
  • WhatsApp-style UI with message bubbles
  • Automatic reconnection handling

Dashboard (dashboard/)

Management interface components:

landingPage.js      // Main dashboard UI
auth.js             // Authentication
repositories.js     // Repo management
configurations.js   // Settings management
observability.js    // Monitoring views

WebSocket Protocol

// Message types
{ type: 'input', data: 'user command' }     // User input
{ type: 'output', data: 'program output' }  // Program output
{ type: 'resize', cols: 80, rows: 24 }      // Terminal resize
{ type: 'ping' }                             // Keep-alive

Data Flow Examples

Starting an Agent

1. User: enable agent demo
   β†’ Find manifest in repos/demo/demo/manifest.json
   β†’ Register in .ploinky/agents.json
   β†’ Generate container name

2. User: start demo 8088
   β†’ Read agents registry
   β†’ Start container for each agent
   β†’ Map ports (container:7000 β†’ host:7001)
   β†’ Update routing.json
   β†’ Start RoutingServer on 8088

3. Container startup:
   β†’ Pull image if needed
   β†’ Mount volumes (workspace, code, Agent)
   β†’ Set environment variables
   β†’ Run agent command or supervisor

API Request Routing

1. Client: GET http://localhost:8088/apis/simulator/monty-hall
   
2. RoutingServer:
   β†’ Extract agent: "simulator"
   β†’ Lookup in routing.json: hostPort: 7002
   β†’ Proxy to: http://localhost:7002/api/monty-hall
   
3. Agent Container:
   β†’ Process request
   β†’ Return response
   
4. RoutingServer:
   β†’ Forward response to client

WebChat Session

1. User: webchat secret python bot.py
   
2. WebTTY Server:
   β†’ Start PTY with command: python bot.py
   β†’ Create HTTP server on port 8080
   β†’ Serve chat.html interface
   
3. Browser connects:
   β†’ WebSocket handshake
   β†’ Authenticate with password
   β†’ Establish bidirectional channel
   
4. Message flow:
   β†’ User types in chat
   β†’ WebSocket β†’ Server β†’ PTY stdin
   β†’ Program output β†’ PTY stdout β†’ WebSocket β†’ Browser
   β†’ Display as chat bubble

Agent MCP Bridge

AgentServer (Agent/server/AgentServer.mjs) expune capabilități prin Model Context Protocol (MCP) folosind transport Streamable HTTP la ruta /mcp pe portul containerului (implicit 7000).

Router ↔ Agent Communication

  • RouterServer abstraction: RouterServer talks to agents through cli/server/AgentClient.js, which wraps MCP transports.
  • MCP protocol: AgentClient builds a StreamableHTTPClientTransport towards http://127.0.0.1:<hostPort>/mcp and exposes listTools(), callTool(), listResources(), and readResource().
  • Unified routing: Requests hitting /mcp carry commands such as list_tools, list_resources, or tool. RouterServer fans these calls out to every registered MCP endpoint and aggregates the replies.
  • Per-agent routes: Legacy paths like /mcps/<agent> remain available for direct calls when needed.
  • Transport independence: RouterServer stays agnostic of protocol details; AgentClient encapsulates the MCP implementation.

Tools and Resources

Agents declare their MCP surface through a JSON file committed alongside the agent source code: .ploinky/repos/<repo>/<agent>/mcp-config.json. When the CLI boots an agent container it copies this file to /tmp/ploinky/mcp-config.json (also keeping /code/mcp-config.json for reference). The file can expose tools, resources, and prompts, and each tool is executed by spawning a shell command. AgentServer does not register anything if the configuration file is missing.

{
  "tools": [
    {
      "name": "list_things",
      "title": "List Things",
      "description": "Enumerate items in a category",
      "command": "node scripts/list-things.js",
      "input": {
        "type": "object",
        "properties": {
          "category": {
            "type": "string",
            "description": "fruits | animals | colors"
          }
        },
        "required": ["category"],
        "additionalProperties": false
      }
    }
  ],
  "resources": [
    {
      "name": "health",
      "uri": "health://status",
      "description": "Service health state",
      "mimeType": "application/json",
      "command": "node scripts/health.js"
    }
  ],
  "prompts": [
    {
      "name": "summarize",
      "description": "Short summary",
      "messages": [
        { "role": "system", "content": "You are a concise analyst." },
        { "role": "user", "content": "${input}" }
      ]
    }
  ]
}

AgentServer pipes a JSON payload to each command via stdin. Tool invocations receive { tool, input, metadata }; resources receive { resource, uri, params }. Command stdout is forwarded to the MCP response, while non-zero exit codes surface as MCP errors.

MCP Task Queue

Longer running MCP tools are orchestrated through Agent/server/TaskQueue.mjs. Every tool execution is wrapped in a task object that captures the command, input payload, timeout hints, timestamps, and eventual result/error.

  • Concurrency guard: AgentServer limits work in flight (default 10, overridable via "maxParallelTasks" in mcp-config.json) and keeps the rest in a FIFO pending queue.
  • Durable state: Tasks are stored in $PWD/.tasksQueue. On restart, pending items resume and previously running entries are rewound to pending so they execute again.
  • Per-task payloads: The queue injects a unique taskId into the JSON delivered to each command so downstream scripts can correlate logs or offer a status channel.
  • Timeout + lifecycle: Tool definitions may specify timeoutMs. The queue arms a timer, kills the underlying process if it runs too long, and marks the task as failed with a timeout message.
  • Response capture: Successful executions persist their stdout (and stderr if present) as MCP content results. Failures store stderr/exit codes so RouterServer surfaces meaningful diagnostics back to the caller.

Task Status Polling

The Router/CLI side uses Agent/client/MCPBrowserClient.js to follow long-running jobs.

  • Polling endpoint: After a task is enqueued, the client hits /mcps/<agent>/task?taskId=... (falling back to /getTaskStatus) every 30 seconds and adds a timestamp query parameter to avoid caching.
  • Incremental updates: Each status response (HTTP 200) updates the console only if the task status changed (pending β†’ running β†’ completed/failed). Terminal states stop the poller immediately.
  • Error handling: Non-200 responses are logged and the poller keeps retrying (except 404 task not found, which stops polling and reports failure), so status checks continue even across transient outages.

Performance Considerations

Container Optimization

  • Reuse existing containers when possible
  • Lazy image pulling
  • Shared base layers between agents
  • Volume mount caching

Network Efficiency

  • Local port mapping avoids network overhead
  • HTTP keep-alive for persistent connections
  • WebSocket for real-time communication
  • Request buffering and batching

Resource Management

  • Automatic container cleanup on exit
  • PID file tracking for process management
  • Log rotation for long-running services
  • Memory-efficient streaming for large outputs