Declarative infrastructure

Apply Kubernetes resources to configure agents, register tool connectors, and deploy services. These changes require no platform redeployment.

Declare the configuration each entity needs in its resource spec. Controllers reconcile supported resource kinds and report their health in status. Agent declarations take effect at turn dispatch, as described below.

Shared resource fields

Each entity uses four fields: apiVersion, kind, metadata, spec . An Agent defines a reusable agent configuration: instructions, a model override, the connector and built-in tools it may call, and its sandbox mode.

YAML
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 (for example 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 agent configuration: 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 service instance: image, port, replicas, and environment.
WorkflowA dependency graph of ServiceDefinition stages, sequenced by dependsOn.
RoutineA scheduled unattended agent turn: a schedule plus a prompt, owned by its creator.
SandboxTemplateThe isolated pod template a conversation runs on.

ToolService provides one example: kubectl apply one and the controller health-checks it, lists its tools, and reflects the result into .status. ServiceDefinition, Workflow, and Routine reconcile the same way. Agent is the exception: applying one takes effect on the next turn, because dispatch reads the declaration live, but no controller watches it. There is no controller validation of the model, connector, or handoff targets, and no controller writes .status. Dispatch and handoff checks still apply at execution time.

Handoff permissions and approval policy

Agent.spec.canHandoffTo lists permitted handoff targets. The control plane enforces this list for conversations bound to an agent; an empty list denies every target. A conversation without a bound agent is not subject to this check. The separate Agent.spec.approvalPolicy field stores intended per-tool approval overrides but has no enforcement yet. Approval instead uses the persona's policy and the capability gate. Do not rely on the agent field to require approval.

Starter templates

The scaffolder creates a ServiceDefinition from a catalog template. Instances share the template image and receive their name through SERVICE_NAME. Each template is a standalone Rust 1.97.1, edition 2024 crate with its own lockfile. Build any of them with docker build -t <name> templates/<name>.

TemplateWhat it is
rust-hello-worldA minimal axum HTTP service with a greeting at / and a /healthz endpoint.
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. Accepts payments from paid_fetch.
rust-tempo-payerA standalone Tempo wallet client that pays for 402-gated resources with spend limits and chain checks.
rust-mcp-toolserviceA minimal MCP tool server (streamable-HTTP) deployable as a ToolService. Use it to start writing a connector.
rust-webhook-edgeA thin edge adapter: verifies a webhook, then drives a turn over the public /v1/chat/completions API.

To test a 402 payment on the testnet, deploy rust-tempo-merchant, point rust-tempo-payer (or an agent’s paid_fetch) at its /resource endpoint.

Services and workflows

To run a template, an agent applies a ServiceDefinition with its image, port, replica count, and environment. The controller creates a Deployment and Service in a namespace with resource quotas. The reconciler sets the security context, resource limits, and probes.

Services that depend on each other form a Workflow: a graph of named stages joined by dependsOn edges. A stage starts only after all its dependencies are ready. For example, a dataset starts before the services that read it. Each stage is a ServiceDefinition with the same security settings.

YAML
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.

Every scaffolded object records who asked for it

ServiceDefinition and Workflow each carry an owner field in the catalog reference grammar, {kind}:{namespace}/{name}, where the namespace is the edge the request arrived through. The scaffolder connector sets this field from the caller's attributed identity at creation. It does not accept an owner supplied by the model. The connector filters list, get, and delete operations by owner.

The connector enforces ownership; the reconciler does not. Direct cluster access bypasses the connector's owner filter.