---
title: "Control API provisioned resources | Tarka Docs"
description: "Apply and inspect desired state for managed Tarka inference services, Agent Hosts, and Batch Jobs."
url: "https://tarkahq.com/docs/control-api/provisioning/"
---

Preview Control API · Provisioning

# Desired-state compute resources

Record typed desired state, keep a stable resource identity, and inspect reconciliation status through organization-scoped resource reads.

**Preview contract, entitlement-gated products.**

Managed inference desired state is published for approved accounts. Agent Hosts and Batch Jobs additionally require an approved `agents` or`jobs` entitlement. Submitting a product-access request does not bypass that gate.

Apply semantics

## Name identifies desired state within a resource type

Managed inference and Agent Host creation methods are apply operations. The tuple `(org_id, resource_type, name)` is unique. Reusing it updates the stored spec, returns the same resource ID, resets lifecycle state to `pending`, and creates an audit entry.

ProvisionedResource `shared response`

```
{
  "id": "448a...",
  "org_id": "9df4...",
  "resource_type": "agent",
  "name": "research-agent",
  "status": "pending",
  "namespace": "",
  "external_url": "",
  "spec": {
    "definition": "compose",
    "template_id": "openclaw",
    "template_version": "2026.2.6-tarka.4",
    "tier": "core",
    "service_count": 2,
    "active_revision": 1,
    "secret_variable_names": ["AUTH_PASSWORD", "OPENCLAW_GATEWAY_TOKEN"]
  },
  "status_detail": {},
  "created_at": "2026-08-30T14:00:00Z",
  "updated_at": "2026-08-30T14:00:00Z"
}
```

**Name** `DNS label`

Lowercase letters, digits, and hyphens; 1–63 characters; start and end with alphanumeric.

**Typed fields win** `stable contract`

Required typed fields override conflicting keys supplied in the free-form `spec` object.

**Status detail** `machine-readable`

Controllers may attach provider state or stable failure information without changing desired spec.

Managed inference

## Apply an inference service

`model_alias` must exist in the active model registry. Replica counts must be non-negative, and a non-zero `max_replicas` must be greater than or equal to `min_replicas`.

Inference service `POST`

```
curl -X POST \
  "https://tarka.rest/control/v1/orgs/$TARKA_ORG_ID/inference-services" \
  -H "Authorization: Bearer $TARKA_ACCOUNT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name":"himalaya-service",
    "model_alias":"himalaya-q8",
    "min_replicas":0,
    "max_replicas":2,
    "spec":{"region":"eu-central"}
  }'
```

The canonical organization route is `/orgs/{org_id}/inference-services`. The compatibility binding `/inference-services` remains available when `org_id` is supplied in the request body.

## Review the price before applying a host

Send the intended request with `dry_run: true` to receive `spec.compute_quote`. Review its billing currency, exact allocated memory, hourly rate and next-lease hold. Submit the same request with `dry_run: false` and `accepted_quote_id` within five minutes. The console requires this confirmation. Existing EU API clients may omit the quote; technical `core` and `pro` sizes are not subscription offers.

Model calls and any separately accepted data policy are additional. Running hosts renew paid leases. See [quotes, reservations and usage policy](https://tarkahq.com/docs/control-api/billing-quotes/).

By request

## Apply an Agent Host stack

Choose exactly one `template_id`, inline `compose_yaml`, or legacy single-container `image`. OpenClaw, Hermes, and Onyx expose runtime defaults, gateways, and variables through the template catalog. Reusing the same name creates an immutable revision.

Template catalog `GET`

```
curl \
  "https://tarka.rest/control/v1/agent-host-templates" \
  -H "Authorization: Bearer $TARKA_ACCOUNT_TOKEN"
```

Agent Host price preview `dry_run: true`

```
# Upload secrets through the authenticated console handoff first.
# The bundle UUID is safe for the assistant to handle; secret values are not.
jq -n \
  --arg bundle "$TARKA_SECRET_BUNDLE_ID" \
  '{
    name: "research-agent",
    template_id: "openclaw",
    tier: "core",
    dry_run: true,
    secrets_bundle_id: $bundle,
    runtime: {primary_model_alias: "deepseek-v4.1-flash-eu"}
  }' |
curl -X POST \
  "https://tarka.rest/control/v1/orgs/$TARKA_ORG_ID/agent-hosts" \
  -H "Authorization: Bearer $TARKA_ACCOUNT_TOKEN" \
  -H "Content-Type: application/json" \
  --data @-
```

**Compose** `compose_yaml`

Multi-service stacks of prebuilt images with health checks, volumes, tmpfs, resources, and routes. Git builds inside Agent Hosts are disabled; build locally or in your own CI and [push to Tarka's registry](https://tarkahq.com/docs/compute/hosted-agents/#tarka-registry) first.

**Variables** `secret: true`

Secret values are encrypted separately and never returned in `ProvisionedResource.spec`.

**Runtime** `primary_model_alias`

Select the host's chat model, supported template gateways, and custom Compose services that receive managed discovery variables.

**Public route** `x-tarka.routes`

Declare path/service/port mappings or one `public_service` and`public_port` override.

Redacted editable configuration `GET`

```
curl \
  "https://tarka.rest/control/v1/orgs/$TARKA_ORG_ID/agent-hosts/$AGENT_HOST_ID/configuration" \
  -H "Authorization: Bearer $TARKA_ACCOUNT_TOKEN"
```

Secret values are empty in this response. A secret with `preserve_existing: true` can be carried into the next revision without returning its plaintext.

Signal QR pairing `Hermes · ready host`

```
curl \
  "https://tarka.rest/control/v1/orgs/$TARKA_ORG_ID/agent-hosts/$AGENT_HOST_ID/gateways/signal/pairing" \
  -H "Authorization: Bearer $TARKA_ACCOUNT_TOKEN"
```

Delete Agent Host `DELETE`

```
curl -X DELETE \
  "https://tarka.rest/control/v1/orgs/$TARKA_ORG_ID/agent-hosts/$AGENT_HOST_ID" \
  -H "Authorization: Bearer $TARKA_ACCOUNT_TOKEN"
```

Custom Compose

## Private GitHub images through REST, MCP, and Python

Put image paths such as `ghcr.io/my-org/my-agent:v1` in your Compose file. Supply credentials separately in `github_registry_credentials`. This field is available only with `compose_yaml`.

Use the token owner's GitHub username and a classic PAT with `read:packages`, access to every referenced package, and organization SSO authorization when required. The username is not the repository or package owner. Your Tarka account token authenticates the Control API; the GitHub PAT is used only for GHCR pulls.

### Named credentials and verified pulls

The console uses `POST /control/v1/orgs/{org_id}/agent-host-registry-tests` with the host `name`, a complete `credentials` array and every GHCR `image` with its Compose `service`. Each credential has a label, username, token and assigned services. To preserve a stored token, send its returned `id` and `version` and omit the token. Removing an entry removes that credential only after a successful test and apply.

Poll `GET /control/v1/orgs/{org_id}/agent-host-registry-tests/{test_id}`. When its status is `succeeded`, include `registry_test_id` in the complete host apply request. Do not combine it with `github_registry_credentials`. The proof expires after 30 minutes, binds to the exact service images and current credential version, and is consumed once by a successful apply. Dry runs do not consume it. Cancel queued or running tests with `DELETE` at the same test URL.

Configuration reads return `registry_credentials` containing IDs, labels, usernames, versions and service assignments only. Starting a credential test is a human action through the console or direct HTTP and is excluded from MCP. MCP can read test status and submit the verified test ID without receiving a PAT.

### Compatible single-credential requests

Create or update a private-image host `REST · Python standard library`

```
import getpass, json, os, pathlib, urllib.request

payload = {
    "name": "private-agent",
    "compose_yaml": pathlib.Path("compose.yaml").read_text(),
    "github_registry_credentials": {
        "username": os.environ["GITHUB_USERNAME"],
        "token": getpass.getpass("GitHub classic PAT: "),
    },
}
url = "https://tarka.rest/control/v1/orgs/" + os.environ["TARKA_ORG_ID"] + "/agent-hosts"
request = urllib.request.Request(url, method="POST",
    data=json.dumps(payload).encode(), headers={
        "Authorization": "Bearer " + os.environ["TARKA_ACCOUNT_TOKEN"],
        "Content-Type": "application/json",
    })
payload["dry_run"] = True
request.data = json.dumps(payload).encode()
with urllib.request.urlopen(request, timeout=60) as response:
    preview = json.load(response)
quote = preview["spec"]["compute_quote"]
print(json.dumps(quote, indent=2))
if not quote["admission"]["available"] or input("Accept this price and create? [yes/no] ") != "yes":
    raise SystemExit("Not created")
payload.update(dry_run=False, accepted_quote_id=quote["id"])
request.data = json.dumps(payload).encode()
with urllib.request.urlopen(request, timeout=60) as response:
    host = json.load(response)
print(host["id"], host["status"])
```

The example reads `compose.yaml` and prompts for the PAT without echoing it. Do not print the request or put credentials in Compose, `spec`, source code, logs, or chat transcripts.

### Preserve, replace, and remove credentials

Apply the same host name and the complete Compose definition on each update. Tarka checks GHCR access before saving; a failed check leaves credentials and the current revision unchanged.

- **Preserve:** omit `github_registry_credentials`. An empty object is not the preserve operation.
- **Replace:** supply both `username` and the new `token`.
- **Remove:** send `{remove: true}` after switching every GHCR image to anonymous access. Do not combine removal with a username or token.

Removal field in the complete apply request `JSON`

```
{
  "github_registry_credentials": {
    "remove": true
  }
}
```

`GET /control/v1/orgs/{org_id}/agent-hosts/{agent_host_id}/configuration` returns only the saved username and configured status. The token is write-only, encrypted at rest, and never returned or included in revision history. The controller provides it only as an image-pull credential, never a container environment variable.

Configuration response fragment `redacted metadata`

```
{
  "github_registry_access": {
    "username": "octocat",
    "configured": true
  }
}
```

Deleting a host removes its saved credentials. Removal does not revoke the PAT at GitHub or stop already running containers. Revoke the PAT at GitHub when required.

### MCP

Connect your MCP client as described in [MCP](https://tarkahq.com/docs/control-api/mcp/). Creating a host needs a connection with write access, `provisioning:read provisioning:write`. Set or change private GHCR credentials yourself through the console or direct HTTP; MCP rejects plaintext registry credentials. Omitted credentials preserve the host's existing pull access. Supply application secrets through a separate handoff, then pass its ID in the complete apply request and review the price before creation.

Apply through MCP `tools/call parameters`

```
{
  "name": "tarka_create_agent_host",
  "arguments": {
    "org_id": "<organization UUID>",
    "name": "private-agent",
    "dry_run": true,
    "compose_yaml": "services:\n  agent:\n    image: ghcr.io/my-org/my-agent:v1\n",
    "secrets_bundle_id": "<uploaded secret handoff UUID>"
  }
}
```

Read back with `tarka_get_agent_host_configuration` and `org_id`/`agent_host_id`. Its `github_registry_access` contains the same redacted metadata. A read-only MCP grant cannot set, replace, or remove credentials.

### Secrets from a file or the console

Prepare a handoff with `POST /control/v1/orgs/{org_id}/agent-host-secret-bundles` and the host's `name`. Open its `console_url` yourself to upload a .env file or a JSON object of string values. The [local secret-file helper](https://github.com/TarkaHQ/api/blob/main/examples/tarka-control-api/scripts/secrets.py) can upload directly from your terminal using your account token in the environment. Do not have the coding agent read the file or paste its contents into chat.

The assistant reads only key names and status, then submits `secrets_bundle_id` with the host request. The handoff expires after one hour and is consumed only by successful admission for the same organization, name and revision. A concurrent host update requires a new handoff. Direct HTTP clients can continue sending inline secrets.

### Python SDK

SDK 0.2.0 adds `AgentHostGitHubRegistryCredentials` and the matching redacted response type. See the [Python credential examples](https://github.com/TarkaHQ/sdk-python/blob/main/docs/control-api.md#custom-compose-and-private-github-images) and [installable release](https://github.com/TarkaHQ/sdk-python/releases/tag/v0.2.0).

For the console workflow, see [Private GitHub images](https://tarkahq.com/docs/compute/hosted-agents/#private-images).

## Tarka registry images

[Push your local build with a Tarka credential](https://tarkahq.com/docs/compute/hosted-agents/#tarka-registry) and reference `registry.tarka.rest/orgs/<organization UUID>/<repository>`. Agent Hosts accept tags and pin the resolved digest; jobs require an explicit digest. The resource and image must belong to the same organization. Pull authorization is automatic; omit `github_registry_credentials` for Tarka images.

By request

## Create a queued Batch Job

`command` is a non-empty argument vector. Jobs execute in US East by default after accepting a current quote. Set `TARKA_JOB_IMAGE` to a public or same-organization Tarka registry image pinned by digest; the example expects your image to contain Python and the `evals.run` module. Resources, runtime/network caps, reads and cancellation are explicit. See [Batch Job availability](https://tarkahq.com/docs/compute/batch-jobs/).

Batch Job `jobs entitlement`

```
import json, os, urllib.request, uuid

base = "https://tarka.rest/control/v1/orgs/" + os.environ["TARKA_ORG_ID"]
headers = {"Authorization": "Bearer " + os.environ["TARKA_ACCOUNT_TOKEN"],
           "Content-Type": "application/json"}
def post(path, body):
    request = urllib.request.Request(base + path, method="POST", headers=headers,
                                     data=json.dumps(body).encode())
    with urllib.request.urlopen(request, timeout=60) as response:
        return json.load(response)

resources = {"cpu_millis": 1000, "memory_mib": 1024, "ephemeral_storage_mib": 1024}
quote = post("/compute-quotes", {"product": "jobs", "location": "us-1",
    "resources": resources, "max_runtime_seconds": 60, "max_network_bytes": 1048576})
print(json.dumps(quote, indent=2))
if not quote["available"] or input("Accept this price and create? [yes/no] ") != "yes":
    raise SystemExit("Not created")
job = post("/jobs", {"name": "nightly-eval", "location": "us-1",
    "image": os.environ["TARKA_JOB_IMAGE"],  # Full Linux/amd64 image@sha256:digest.
    "command": ["python", "-m", "evals.run"], "resources": resources,
    "timeout_seconds": 60, "max_network_bytes": 1048576,
    "queue_timeout_seconds": 600, "accepted_quote_id": quote["id"],
    "idempotency_key": str(uuid.uuid4())})
print(job["id"], job["status"])
```

Read state

## List or retrieve shared resource state

Use `GET /orgs/{org_id}/resources` and optional `?resource_type=inference_service`, `agent`, `job`, `sandbox`, `bucket`, or `git_repo`. Retrieve one record with `GET /orgs/{org_id}/resources/{resource_id}`.

| Status | Meaning |
| --- | --- |
| `pending` | Desired state is accepted and awaits reconciliation. |
| `provisioning` | A controller is applying provider or cluster state. |
| `ready` | The resource is reconciled and usable. |
| `failed` | Reconciliation stopped; inspect `status_detail`. |
| `deleting` | Product-specific deletion is in progress. |
| `deleted` | The data-plane resource is removed; final control state is retained. |

**No generic delete is published.**

Object Storage, Hosted Git, Sandboxes, and Agent Hosts have product-specific deletion methods. Managed inference has no generic delete. Batch Jobs have explicit run cancellation; read the final run state to confirm work stopped.
