Agent APIs

API endpoints for agent registration, lifecycle management, and task coordination.

Status: Partial implementation (registration/lifecycle endpoints, durable Postgres agent store, internal CA bootstrap, external task polling/result runtime, and local compose bootstrap implemented; full containerized E2E remains tracked) Source: Architecture Advisory Section 6.3.2 Related Modules: Agents Module, Agent Security

Overview

The Agent API provides endpoints for registering deployment agents, managing their lifecycle, and coordinating task execution. Agents use mTLS for secure communication after initial registration.

Implementation note (2026-05-09): the standalone ReleaseOrchestrator WebApi registers the production IAgentTaskTransport DI boundary and an unhealthy fail-closed readiness check when no verified transport implementation is present. When ReleaseOrchestrator:AgentTaskTransport:Enabled=true, WebApi registers the mTLS polling runtime below. Agent registration and task state are durable through PostgresAgentStore / PostgresAgentTaskStore; registered agents are written to release.agents, which is the same table read by the topology catalog. Registered agents receive short-lived client certificates signed by the Release Orchestrator internal agent CA. Certificate issuance must complete before an active agent row is persisted; the store writes the agent row and public certificate PEM together so CA/bootstrap failures do not leave certificate-less active agents. The CA root is file-backed through ReleaseOrchestrator:AgentCertificateAuthority:RootPfxPath; set RootPfxPassword from a mounted secret and mount the PFX path on durable storage in compose or production.


Registration Endpoints

Create Registration Token

Endpoint: POST /api/v1/release-orchestrator/agents/registration-tokens

Creates a one-time registration token. Requires Release Orchestrator operate authorization and tenant context.

Headers:

Authorization: Bearer {operator-token}
X-StellaOps-TenantId: {tenant-id}

Request:

{
  "name": "agent-prod-01",
  "displayName": "Agent Prod 01",
  "capabilities": ["docker", "compose"],
  "validFor": "24:00:00"
}

Response: 201 Created

{
  "id": "uuid",
  "name": "agent-prod-01",
  "displayName": "Agent Prod 01",
  "capabilities": ["Docker", "Compose"],
  "token": "one-time-token",
  "expiresAt": "2026-01-11T14:23:45Z",
  "createdAt": "2026-01-10T14:23:45Z"
}

Register Agent

Endpoint: POST /api/v1/release-orchestrator/agents

Consumes a one-time registration token and returns the agent id plus a one-time PEM bundle. The standalone agent-core host stores the returned certificate and key on a private local secret mount before it starts polling.

Request:

{
  "token": "one-time-token",
  "hostInfo": {
    "hostname": "agent-prod-01",
    "platform": "compose",
    "version": "dev"
  },
  "labels": {
    "datacenter": "us-east-1",
    "role": "deployment"
  }
}

Response: 201 Created

{
  "id": "uuid",
  "name": "agent-prod-01",
  "displayName": "Agent Prod 01",
  "status": "Active",
  "capabilities": ["Docker", "Compose"],
  "hostname": "agent-prod-01",
  "version": "dev",
  "certificateThumbprint": "ABC123",
  "certificateExpiresAt": "2026-01-11T14:23:45Z",
  "certificatePem": "-----BEGIN CERTIFICATE-----...",
  "privateKeyPem": "-----BEGIN PRIVATE KEY-----...",
  "registeredAt": "2026-01-10T14:23:45Z"
}

Notes:

Local Compose Launch

The local compose lane provides agent-core under the agents profile in devops/compose/docker-compose.stella-services.yml. The normal local entry point is the launcher:

devops\compose\scripts\launch-agent-core.ps1

The launcher reuses devops/compose/env/agent-core.local.env when local enrollment material already exists, registers only when needed, starts agent-core through the canonical compose chain, verifies the container Agent__AgentId, and waits for a Release Orchestrator heartbeat. Use agent-bootstrap.ps1 -NoComposeUp or agent-bootstrap.sh --no-compose-up only when another installer owns process launch.

The Console’s Enroll a host and topology-wizard surfaces expose this bootstrap path and the real enrolled-agent registry. They deliberately do not offer a registration-only “in-process lab agent”: minting a token and creating an agent row without starting a polling process produces an executor that cannot claim deployment tasks. A host is presented as enrolled execution capacity only after the real bootstrap/launcher starts its agent and the registry reports its heartbeat.

The scripts write ignored local files under devops/compose/env/agent-core.local.env and devops/compose/offline/agent-core/. See devops/compose/agent-bootstrap.md.

For Bash-only lanes, the bootstrap script can still enroll and start the service through the compose helper:

devops/compose/scripts/agent-bootstrap.sh

Execution Plugin Overlays

First-party execution plugin payloads are mounted bundles, not agent image contents. Agent-core image rebuilds still carry the host runtime, required CLIs, and the sandbox guest launcher at /app/tools/StellaOps.Plugin.Host; publish-first image helpers stage that tool separately from the mounted execution plugin bundles. Generate the local signed profiles before rendering or starting an execution-plugin agent:

powershell -NoProfile -ExecutionPolicy Bypass -File .\devops\build\package-execution-plugins.ps1 -Profile default
powershell -NoProfile -ExecutionPolicy Bypass -File .\devops\build\package-execution-plugins.ps1 -Profile optional-build-script

Render the default profile through the compose wrapper with the profile-gated agent service enabled:

$env:COMPOSE_PROFILES = 'agents'
$env:COMPOSE_EXTRA_FILES = 'docker-compose.plugins.execution.yml'
.\devops\compose\scripts\compose-cli.ps1 config --format json

The default overlay mounts devops/plugins/execution/default and the execution trust root read-only into agent-core, and declares only build.docker, deploy.compose.up, deploy.exec, deploy.artifact-extract, and deploy.docker-plugin beyond the platform transport capabilities. The launcher defaults to its historical lab overlay for compatibility; use -ExecutionPluginOverlay default when proving the production-shaped default bundle set. Add the optional overlay only when the operator accepts operator-supplied build-script containers and Ansible playbook obligations:

$env:COMPOSE_PROFILES = 'agents'
$env:COMPOSE_EXTRA_FILES = 'docker-compose.plugins.execution.yml;docker-compose.plugins.execution-optional.yml'
.\devops\compose\scripts\compose-cli.ps1 config --format json

Do not persist full compose renders as QA artifacts; they can include local environment values. Store sanitized agent-core plugin env keys and plugin-related volume targets instead.

For Sprint 017 BLD-003 live proof, build an explicit reachability-capable agent image and enable the runtime switches only for that run:

$env:AGENT_CORE_INCLUDE_BUILD_REACHABILITY = 'true'
$env:AGENT_CORE_BUILD_COPRODUCTION_ENABLED = 'true'
$env:AGENT_CORE_BUILD_REACHABILITY_PRODUCER_ENABLED = 'true'

Lifecycle Endpoints

List Agents

Endpoint: GET /api/v1/release-orchestrator/agents

Query Parameters:

Response: 200 OK

[
  {
    "id": "uuid",
    "name": "agent-prod-01",
    "version": "1.0.0",
    "status": "online",
    "capabilities": ["docker", "compose"],
    "lastHeartbeat": "2026-01-10T14:23:45Z",
    "resourceUsage": {
      "cpu": 15.5,
      "memory": 45.2
    }
  }
]

Get Agent

Endpoint: GET /api/v1/release-orchestrator/agents/{id}

Response: 200 OK - Full agent details including assigned targets

Update Agent

Endpoint: PUT /api/v1/release-orchestrator/agents/{id}

Request:

{
  "labels": {
    "datacenter": "us-west-2"
  },
  "capabilities": ["docker", "compose", "ssh"]
}

Response: 200 OK - Updated agent

Delete Agent

Endpoint: DELETE /api/v1/release-orchestrator/agents/{id}

Revokes agent credentials and removes registration.

Response: 200 OK

{ "deleted": true }

Heartbeat Endpoints

Send Heartbeat

Endpoint: POST /api/v1/agents/{id}/heartbeat

Agents must send heartbeats at the configured interval to maintain online status and receive pending tasks.

Request:

{
  "status": "healthy",
  "resourceUsage": {
    "cpu": 15.5,
    "memory": 45.2,
    "disk": 60.0
  },
  "capabilities": ["docker", "compose"],
  "runningTasks": 2
}

Response: 200 OK

{
  "tasks": [
    {
      "taskId": "uuid",
      "taskType": "docker.pull",
      "payload": {
        "image": "myapp",
        "tag": "v2.3.1",
        "digest": "sha256:abc123..."
      },
      "credentials": {
        "registry.username": "user",
        "registry.password": "token"
      },
      "timeout": 300
    }
  ],
  "certificateRenewal": {
    "cert": "-----BEGIN CERTIFICATE-----...",
    "expiresAt": "2026-01-11T14:23:45Z"
  }
}

Notes:


Task Endpoints

Poll Task (implemented)

Endpoint: POST /api/v1/release-orchestrator/agent-runtime/agents/{agentId}/tasks:poll

Agents poll this endpoint over mTLS to receive one pending assignment. The route agentId, body agentId, and mTLS certificate thumbprint must all bind to the same registered active agent. The agent must declare the assignment’s required capability before the assignment is returned.

Request:

{
  "contractVersion": "agent-task-transport/v1",
  "agentId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
  "capabilities": ["compose"]
}

Response: 200 OK

{
  "contractVersion": "agent-task-transport/v1",
  "assignment": {
    "contractVersion": "agent-task-transport/v1",
    "assignmentId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
    "taskId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
    "agentId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
    "requiredCapability": "compose",
    "taskType": "compose.up",
    "payload": "{\"projectName\":\"payments\",\"composeLock\":\"services: {}\"}",
    "credentials": {},
    "variables": { "projectName": "payments" },
    "assignedAt": "2026-04-29T11:00:00Z",
    "timeout": "00:05:00"
  }
}

Other responses:

Submit Signed Task Result (implemented)

Endpoint: POST /api/v1/release-orchestrator/agent-runtime/agents/{agentId}/tasks/{taskId}/result

The result is accepted only when the route task id equals the body taskId, the agent id matches the mTLS-bound assignment, and signature verifies over the stable result payload with the mTLS certificate public key. Supported signature algorithms are rsa-sha256 and ecdsa-sha256.

Request:

{
  "contractVersion": "agent-task-transport/v1",
  "agentId": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
  "taskId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
  "success": true,
  "message": "completed",
  "resultData": "{\"container\":\"ok\"}",
  "duration": "00:00:03",
  "completedAt": "2026-04-29T11:00:03Z",
  "signatureAlgorithm": "rsa-sha256",
  "signature": "base64-signature"
}

Response: 202 Accepted

{
  "status": "accepted",
  "taskId": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
}

Failure behavior: unsigned, invalidly signed, mismatched-agent, mismatched-task, stale, timed-out, or unconfigured transport results are rejected and cannot advance deployment state to success.

Complete Task

Endpoint: POST /api/v1/agents/{id}/tasks/{taskId}/complete

Reports task completion status back to the orchestrator.

Request:

{
  "success": true,
  "result": {
    "imageId": "sha256:abc123...",
    "containerId": "container-uuid"
  },
  "logs": [
    { "timestamp": "2026-01-10T14:23:45Z", "level": "info", "message": "Pulling image..." },
    { "timestamp": "2026-01-10T14:23:50Z", "level": "info", "message": "Image pulled successfully" }
  ]
}

Response: 200 OK

{ "acknowledged": true }

Get Pending Tasks

Endpoint: GET /api/v1/agents/{id}/tasks

Alternative to heartbeat for polling pending tasks.

Response: 200 OK

{
  "tasks": [
    {
      "taskId": "uuid",
      "taskType": "docker.run",
      "priority": 10,
      "createdAt": "2026-01-10T14:20:00Z"
    }
  ]
}

WebSocket Endpoints

Task Stream

Endpoint: WS /api/v1/agents/{id}/task-stream

Real-time task assignment stream for agents.

Messages (Server to Agent):

{ "type": "task_assigned", "task": { "taskId": "uuid", "taskType": "docker.pull", ... } }
{ "type": "task_cancelled", "taskId": "uuid" }

Messages (Agent to Server):

{ "type": "task_progress", "taskId": "uuid", "progress": 50, "message": "Pulling layer 3/5" }
{ "type": "task_log", "taskId": "uuid", "level": "info", "message": "..." }

Error Responses

Status CodeDescription
401Invalid or expired registration token
403Agent not authorized for this operation
404Agent not found
409Agent name already registered
503Agent offline or unreachable

See Also