harbor

Remote Rollouts

Submitting Jobs

Launch a remote rollout with POST /job-submit and check its status

POST /job-submit launches a remote rollout.

The examples below use $BASE and $KEY. Get a key from Harbor Hub API Key.

export BASE=https://ofhuhcpkvzjlejydnvyd.supabase.co/functions/v1
export KEY=sk-harbor-...

Request headers:

Authorization: Bearer sk-harbor-...
Content-Type: application/json
Idempotency-Key: <unique-retry-key>

The Idempotency-Key header is required. Requests without it are rejected, and the key may be at most 200 characters. Reusing a key returns the job that key already created instead of launching a second one, so generate a fresh key per launch and reuse it only when retrying that same launch. If Idempotency-Key is reused, the server will return the previous launch URL even if the config changes.

The top level of the request body takes these fields:

FieldRequiredNotes
configyesThe job configuration
organizationnoName of the organization that will own the job. Defaults to your personal org
job_secretsnoOne-off environment variable name to secret value. Added just for this job, and injected only into agents whose secrets sub-field name it
registry_credentialsnoRegistry host to stored credential name or ID "us-east1-docker.pkg.dev": "registry-cred-stored-name"
dry_runnoValidate and resolve everything without creating a job. Defaults to false

job_secrets and registry_credentials are siblings of config, not fields inside it. This is deliberate: it keeps plaintext secrets out of the job config that gets stored and replayed. Placing either one inside config is rejected. Upon submission these credentials are routed through an encryptor and decrypted only at runtime. Plaintext secrets are never stored on our platform.

Here is a launch using a registry dataset and a built-in agent:

{
  "config": {
    "job_name": "I-love-harbor",
    "agents": [
      {
        "name": "terminus-2",
        "model_name": "openai/gpt-5.6-luna",
        "secrets": ["OPENAI_API_KEY"]
      }
    ],
    "datasets": [
      {
        "name": "harbor/hello-world",
        "ref": "latest",
        "n_tasks": 5
      }
    ],
    "n_attempts": 1,
    "n_concurrent_trials": 20
  },
  "job_secrets": {
    "OPENAI_API_KEY": "sk-..."
  },
  "dry_run": false
}

A successful launch returns the job and its viewer URL:

{
  "job_id": "00000000-0000-0000-0000-000000000001",
  "job_name": "I-love-harbor",
  "viewer_url": "https://hub.harborframework.com/jobs/00000000-0000-0000-0000-000000000001",
  "status_url": "https://hub.harborframework.com/jobs/00000000-0000-0000-0000-000000000001",
  "n_trials": 1,
  "owner_org": { "id": "...", "name": "..." },
  "agent_sources": []
}

owner_org reports which organization ended up owning the job, and agent_sources echoes any resolved custom agent sources including their pinned commits. On a dry run no job is created, so job_id comes back null, but every source is still resolved and validated.

To submit a request stored in launch.json:

BASE="https://ofhuhcpkvzjlejydnvyd.supabase.co/functions/v1"
KEY="sk-harbor-..."

curl -sS -X POST "$BASE/job-submit" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d @launch.json

Job Config

config mirrors the JobConfig the CLI sends, so unrecognized keys pass through untouched. The fields the API validates directly are:

FieldRequiredNotes
agentsyesAt least one agent
tasksnoIndividual tasks
datasetsnoRegistry or git repo datasets
job_namenoDefaults to a UTC timestamp such as 2026-08-13__14-30-00 (this timestamp can be super annoying to look through later. So seriously consider setting a name)
n_attemptsnoBetween 1 and 10. Defaults to 1
n_concurrent_trialsnoBetween 1 and 1000. Defaults to 4
credential_modenogateway (default) or direct. gateway uses credential proxying. direct directly injects decrypted secrets into the task container
retrynoinclude_exceptions and exclude_exceptions lists

You need at least one of tasks or datasets, and together they must resolve to at least one task. The total number of trials is n_attempts × tasks × agents, and must land between 1 and 50,000.

On each agent, name is the Harbor agent type, model_name is the model it uses, and secrets is the list of credentials that agent receives. Agent versions installed from a package manager are pinned when the job is submitted, so every trial in a job installs the same release even if the job sits queued across an upstream publish.

Agent Secrets

Credentials are scoped per agent, not per job. Every agent carries its own secrets list naming the environment variables it receives, so two agents in the same launch can be given different keys.

{
  "name": "claude-code",
  "model_name": "anthropic/claude-opus-4-1",
  "secrets": ["ANTHROPIC_API_KEY", "HF_TOKEN"]
}

Each name must look like an environment variable, matching ^[A-Z][A-Z0-9_]{0,63}$, and at most 64 of them may be selected per-agent. Secrets are matched to their values either from stored organization secrets, or supplied through the launch's job_secrets. When there is a secret supplied through job_secrets and stored in the organization secrets, the job_secrets submission wins. A selected name backed by neither carries no value of its own.

For example, if I launch a job with the following config snippet:

{
  "name": "claude-code",
  "model_name": "anthropic/claude-opus-4-1",
  "secrets": ["ANTHROPIC_API_KEY"]
}
"job_secrets": {}

and ANTHROPIC_API_KEY is not set on my Harbor Hub organization, the variable ANTHROPIC_API_KEY=None would be injected into the agent's task environment. If ANTHROPIC_API_KEY is set on my Harbor Hub organization, it will be appropriately injected.

If I instead launch with the following config snippet:

{
  "name": "claude-code",
  "model_name": "anthropic/claude-opus-4-1",
  "secrets": ["ANTHROPIC_API_KEY"]
}
"job_secrets": {"ANTHROPIC_API_KEY": "sk-ant-job-secret"}

Then the value provided in job_secrets (or a proxy) will be injected into the agent env regardless of whether or not the same credential name is stored on my Harbor Hub organization.

You should send the secrets field for every agent.

{
  "name": "claude-code",
  "model_name": "anthropic/claude-opus-4-1",
  "secrets": ["ANTHROPIC_API_KEY"]
}
secretsWhat the agent receives
[A, list, of, names]Exactly those names, as far as each one resolves
[]Nothing. The agent starts with no credentials at all

The oracle and nop agents need no inference credential and can be launched with "secrets": [].

Under the default gateway credential mode, a selected credential for openai, anthropic, openrouter, xai, or gemini arrives as a proxy capability rather than your real key, injected under its own name alongside the provider's standard variables so an agent's usual lookup finds it. Everything else you select is injected under its own name with its real value. Credentials for vercel_ai_gateway and devin require "credential_mode": "direct"; in gateway mode they are dropped.

Secrets should not be placed in an agent's env. That is only for nonsensitive environment variables.

{
  "name": "claude-code",
  "model_name": "anthropic/claude-opus-4-1",
  "env": [], // secrets do NOT go here
  "secrets": ["ANTHROPIC_API_KEY"] // secret names go HERE
}
"job_secrets": {"ANTHROPIC_API_KEY": "sk-ant-job-secret"} // secret {"key": value} go HERE

Tasks

Use tasks for individual tasks. Each entry is either a registry task or a git task, never both.

A registry task takes a name in org/name form and an optional ref, which is a version ref like "latest", or a semantic version string.

A git task takes a git_url and a path, where path is the task directory relative to the repository root and is required. Pin the revision with either git_ref (a branch, tag, or SHA) or git_commit_id (a resolved 40-character SHA), but not both. Git tasks use those fields rather than ref, and local filesystem paths cannot be used for remote rollouts.

{
  "config": {
    "job_name": "individual-tasks",
    "agents": [
      {
        "name": "terminus-2",
        "model_name": "openai/gpt-5-mini",
        "secrets": ["OPENAI_API_KEY"]
      }
    ],
    "tasks": [
      {
        "git_url": "https://github.com/owner/private-tasks.git",
        "git_ref": "main",
        "path": "tasks/my-task"
      },
      {
        "name": "terminal-bench/torch-tensor-parallelism",
        "ref": "latest"
      }
    ]
  },
  "job_secrets": {
    "OPENAI_API_KEY": "sk-..."
  },
  "dry_run": false
}

Private GitHub repositories have to be connected or shared through the profile settings flow before they can be used as a task or agent source. TODO: add pics here.

Datasets

Use datasets for registry or git repo datasets.

A registry dataset takes a name in org/name form and either ref or version, but never both. You can narrow it with n_tasks to cap how many tasks are drawn, and with the task_names and exclude_task_names glob patterns.

A git repo dataset uses Harbor's repo source string, the same grammar as harbor run --repo. That accepts org/name, github.com/org/name, a full https or ssh URL, or a GitHub /tree/<ref>/<subdir> URL, each with an optional trailing @<branch|tag|sha>. The optional path is the tasks directory relative to the repository root, which Harbor defaults to tasks.

{
  "datasets": [
    {
      "repo": "my-org/my-tasks@main",
      "path": "tasks",
      "n_tasks": 5
    }
  ]
}

Git repo datasets pin their revision inside the repo string, so they do not take ref or version. They also do not accept git_url, git_ref, or git_commit_id — those belong to tasks.

Whatever a latest or a tag resolves to is pinned into the stored config, so a saved job records the concrete version that actually ran rather than a moving reference.

Job Secrets

job_secrets is a flat map of environment variable name to value:

{ "OPENAI_API_KEY": "sk-..." }

Names must be uppercase and start with a letter, values must be non-empty and at most 16,384 characters, and you can send at most 50 entries. Values are encrypted before storage and never returned.

Secret-looking environment variables found inside config are automatically moved into this encrypted channel before anything is saved, so plaintext never lands in a stored config. If the same name appears in both places, the explicit job_secrets entry wins.

Sending a value here makes it available to the job; it does not decide which agent gets it. Name it in that agent's secrets too, unless you are relying on the omitted-secrets fallback for the model's canonical key.

You can also store a key once as a hosted secret and leave it out of the launch entirely. Stored secrets are still selected the same way, by naming them in secrets, and are read from the organization that owns the job.

GET /job-status returns trial counts for one or more jobs.

curl -sS "$BASE/job-status?job_id=<uuid>" \
  -H "Authorization: Bearer $KEY"

Pass job_id once per job to check several at a time, or a comma-separated job_ids list. Add force_combined=true to get the combined overview shape even for a single job. The same request works as a POST with a {"job_ids": [...], "force_combined": false} body.

A single-job response reports pending, running, completed, failed, canceled, and total.

On this page