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 / WebChat / Dashboard / Status"]
CLI["Ploinky CLI Core
Command Handler / Service Manager / Config"]
RS["Routing Server
+ Watchdog"]
WS["Web Services
WebChat / Dashboard / Status"]
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
}
}
Shared Utilities and Sandbox Backends
| Service | Responsibility |
|---|---|
cli/utils/workspace.js |
Manages .ploinky directory and configuration |
cli/sandbox/docker/ |
Container lifecycle management modules (runtime helpers, interactive commands, agent management) |
cli/utils/repos.js |
Repository management and agent discovery |
cli/utils/agents.js |
Agent registration and configuration |
cli/utils/security/secretVars.js |
Environment variable and secrets management |
cli/utils/config.js |
Global configuration constants |
cli/commands/help.js |
Help system and documentation |
cli/sandbox/bwrap/ |
Bubblewrap sandbox lifecycle management (Linux) |
cli/sandbox/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
- Client sends request to
http://localhost:8088/apis/agent1/method - RoutingServer extracts agent name from path
- Looks up agent's container port in routing.json
- Proxies request to
http://localhost:7001/api/method - Returns response to client
Agent Static Serving
The router no longer reads agent application files from the host filesystem. It owns workspace-global routes such as /workspace-files and the management surfaces, then proxies agent-prefixed requests to the selected agent. The agent receives the path with the mount prefix removed and is responsible for serving its own application files.
GET /index.html β 302 /<static-agent>/index.html
GET /explorer/index.html β explorer upstream /index.html
GET /explorer/assets/app.js β explorer upstream /assets/app.js
GET /workspace-files/doc.md β router workspace-file handler
The shared AgentServer serves static files from PLOINKY_CODE_DIR or /code after its built-in endpoints. Agents that use a custom manifest.agent command must implement equivalent static serving themselves.
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
/healthevery 30s, restart after 3 consecutive failures. - Container monitoring: Polls container state every 5 seconds.
- Manual activation: Agents with
startup: manualare monitored only after explicit activation has created a route; stopped manual agents lose stale routes during general startup. Continuing a WebChat task is explicit activation of its stored provider: the router uses the internal global enable lifecycle and waits for readiness before resuming the task. - Maintenance coordination: Defers automatic container restarts when the same container has an active maintenance lock, including timers scheduled before reinstall or explicit restart began.
- 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)
βββ 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": "<workspace>/.data/demo",
"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 agent runtime key (PLOINKY_DERIVED_MASTER_KEY) is derived from PLOINKY_MASTER_KEY and also roots agent-owned generated secrets declared with generatedSecret: true. Replay protection is enforced via a memory cache.
Web Services Architecture
WebChat (cli/server/handlers/webchat/)
Chat interface for CLI programs:
index.jsauthenticates and dispatches WebChat requests.- Conversation, upload, workspace-suggestion, message-envelope, and runtime responsibilities are isolated in dedicated modules.
- Captures stdout/stdin through the selected agent's TTY implementation.
- Uses HTTP input requests and server-sent events for real-time output.
- 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. Router WebChat handler:
β Start PTY for the selected agent CLI
β Serve chat.html through the router
3. Browser connects:
β Open the server-sent event stream
β Authenticate through the router login flow
β Reuse the folder-session runtime across browser tabs
4. Message flow:
β User types in chat
β HTTP POST β Server β TTY stdin
β Program output β TTY stdout β SSE β 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
StreamableHTTPClientTransporttowardshttp://127.0.0.1:<hostPort>/mcpand exposeslistTools(),callTool(),listResources(), andreadResource(). - Unified routing: Requests hitting
/mcpcarry commands such aslist_tools,list_resources, ortool. RouterServer fans these calls out to every registered MCP endpoint and aggregates the replies. - Per-agent routes: Agent-prefixed paths like
/<agent>/mcpprovide direct access 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"inmcp-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
taskIdinto 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 and live-log channels: Successful executions persist stdout as the MCP result and use stderr for the live task log. Logs are bounded unless the tool declares full retention. A structured stdout object containing
outputTextis reduced to that field before exposure; a validated continuation descriptor may be retained as result metadata without entering visible task output. Failures retain stderr and exit-code diagnostics.
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
/<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.
AgentMcpClient.callTool() owns blocking task polling and returns the terminal result. Its separate callToolWithoutWait() path returns the initial task response and offers it to a registered process-local observer without starting client-owned polling. A WebChat-aware CLI can use that observer to detach the task, continue polling through the router, and emit generic task envelopes. WebChat stores metadata and logs separately, inserts one lightweight { type: "task", taskId } conversation item per started task after the active assistant placeholder, broadcasts task-update over its existing EventSource stream, and retains a disconnected watcher runtime while its tasks remain ongoing. The browser resolves each id into a compact task reference after live updates or history loading. Its link opens an authenticated task view in the generic right-side iframe; the parent forwards only that task's existing stream updates through same-origin postMessage, and the view uses the task APIs for initial state and log-offset recovery. The view does not create another EventSource. A terminal task with provider-issued continuation metadata can start another authenticated remote execution from this page while retaining the same local task id; a monotonically increasing turn selects the current remote task and late prior-turn updates are ignored. Ploinky persists only the live log stream and never concatenates the terminal MCP result into that log. Providers emit final answers through the live stream when those answers belong in the task item. Indexed insertion preserves user β assistant β task items order when task events arrive before final assistant text.
Both AgentMcpClient and MCPBrowserClient expose getAgentStatus() and ensureAgentRunning(). The latter reads Marketplace state first, avoids a repeated enable request when the target already runs, otherwise submits the existing enable_agent action, and waits for live runtime status before the caller invokes the target. Enable mode is caller-controlled: the clients include mode only when supplied and otherwise leave Marketplace's isolated default intact. Agent-side calls use request-signed assertions; browser-side calls reuse the authenticated session and therefore retain normal Marketplace administrator checks.
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