Declarative infrastructure

Add an agent, register a tool connector, or stand up a service by applying a declaration — never by redeploying the platform. Every declaration shares one Kubernetes-shaped envelope.

An agent's persona, a tool connector, a running service, and a multi-stage pipeline of them are each a declarative entity. The control plane reconciles the declaration and reflects its health into status — no redeploy of the control plane or the harness.

One envelope for every entity

Each entity carries the same four fields — apiVersion, kind, metadata, spec — so wildly different things share one format. An Agent is a reusable persona: instructions, a model override, the connector and built-in tools it may call, and its sandbox mode.

apiVersion: polychrome.dev/v1alpha1
kind: Agent
metadata:
  name: ops
  namespace: polychrome
spec:
  description: Deploy, list, and tear down service instances via the scaffold connector.
  instructions: |
    You are Polychrome, managing service instances. Use the scaffold
    connector's tools to list, deploy, check, or tear down instances.
  toolsEnabled:
    - scaffold
  builtinTools: []
  sandboxMode: read-only

A Conversation names an agent by its bare metadata.name (e.g. agentId: ops); the control plane resolves it at turn dispatch and applies its instructions, model, and tool scope instead of reading inline conversation fields.

The entity model

EntityDescribes
AgentA reusable persona: instructions, a model override, the connector and built-in tools it may call, and its sandbox mode.
Tool / ToolServiceA built-in pure tool, or a private MCP server registered as a catalog entry.
ServiceDefinitionA running instance of a template — image, port, replicas, environment.
WorkflowA dependency graph of ServiceDefinition stages, sequenced by dependsOn.
RoutineAn operator-published, schedule-bound unattended-routine definition.
SandboxTemplateThe isolated pod template a conversation runs on.

ToolService is the exemplar: kubectl apply one and the controller health-checks it, lists its tools, and reflects the result into .status. Agent, ServiceDefinition, Workflow, and Routine all reconcile the same way.

Two fields that are declared but not yet enforced

Agent.spec also carries canHandoffTo (which other agents this one may hand off to) and approvalPolicy (tool names that should always require signed human approval for this agent). Both parse and store today, and neither is checked yet: a handoff to an undeclared agent isn't refused, and approval is actually decided by the separate, already-shipped capability gate — not by this field. Treat them as reserved, not as a security boundary, until enforcement lands.

Starter templates

The catalog ships deployable skeletons the scaffolder stamps out as a ServiceDefinition — one shared image per template, with the instance name injected via SERVICE_NAME. Each is a standalone Rust 1.95 / edition 2024 crate that builds on its own lockfile, so a template never couples to the platform's dependencies. Build any of them with docker build -t <name> templates/<name>.

TemplateWhat it is
rust-hello-worldA minimal axum HTTP service (/ greeting + /healthz) — the catalog’s starting point.
rust-datasetA minimal axum data source with /data and /healthz. Other workflow stages depend on it.
rust-tempo-merchantAn x402 / Machine Payments Protocol paywalled resource on the Tempo testnet — the inbound (server) half of the rail paid_fetch pays into.
rust-tempo-payerA standalone Tempo wallet and 402 payer — the outbound half: settles 402-gated resources, capped and chain-guarded.
rust-mcp-toolserviceA minimal MCP tool server (streamable-HTTP) deployable as a ToolService — the skeleton for writing a connector.
rust-webhook-edgeA thin edge adapter: verifies a webhook, then drives a turn over the public /v1/chat/completions API.

The two Tempo templates pair up: deploy rust-tempo-merchant, point rust-tempo-payer (or an agent’s paid_fetch) at its /resource, and the two exercise a full 402 settle loop on the testnet.

Services and workflows

A template is a skeleton. To run one, an agent applies a single ServiceDefinition: the image, port, replica count, and environment, and nothing more. The controller expands it into a hardened Deployment and Service in a quota-bounded namespace. The agent never writes raw manifests, and the reconciler sets every operational detail — the security context, resource limits, and probes. A scaffolded workload is as locked down as an operator’s.

Services that depend on each other form a Workflow: a graph of named stages joined by dependsOn edges. The platform enforces the dependency instead of merely recording it. A stage starts only after every stage it depends on is ready, so a dataset comes up before the services that read it. Each stage is itself a ServiceDefinition and inherits the same hardening. The Workflow adds the graph and the gate.

apiVersion: polychrome.dev/v1alpha1
kind: Workflow
metadata:
  name: demo-pipeline
  namespace: polychrome-apps
spec:
  stages:
    - name: data                  # upstream data source; starts first
      kind: dataset
      template: rust-dataset
      image: ghcr.io/officialunofficial/dataset-rust:latest
    - name: api                   # starts after data is ready
      kind: service
      template: rust-hello-world
      image: ghcr.io/officialunofficial/hello-world-rust:latest
      dependsOn: [data]

The platform reports each stage’s progress in status.stages[], in dependency order: Blocked, then Pending, then Ready. An agent follows the pipeline from start to finish without reading the underlying Deployments.

The scaffolder connector provides these as tools. Use template_list to browse the catalog, workflow_render to preview a dry run that validates the graph, and workflow_create and workflow_status to create and follow a pipeline. The single-service tools, service_create and service_status, work the same way. Creating or deleting requires a signed approval. The read-only tools don’t.

Roadmap: a queryable relation graph

The catalog's design goes further than what's shipped: a stable {kind}:{namespace}/{name} reference grammar, an Edge entity for the surfaces that front an agent, a Model entity for named backends, a Tenant entity for multi-tenancy and caller provenance, and a relation graph derived from all of it — so questions like "which edges can reach the payments tool" or "did any conversation hand off somewhere it wasn't declared to reach" are answerable before a turn runs, or checked against the signed event log after one does. None of that is built yet; it's proposed, not implemented. The reference grammar itself is real in one place today — ServiceDefinition and Workflow stamp an owner field in that shape (e.g. user:slack/U123) from the caller's attributed identity, though nothing resolves it against a Tenant object because none exists.