Evaluate agents on ACK with the Harbor CLI

Updated at:

Agent evaluation requires running many tasks in isolation and collecting their results. The Container Service for Kubernetes (ACK) environment of the Harbor CLI schedules each task as a separate pod in your cluster, and supports mounting datasets from Object Storage Service (OSS), pre-building images, and collecting results automatically.

Harbor (an open-source framework for agent evaluation, not the container image registry) uses the harbor run command to create a separate trial pod for each evaluation task in an ACK cluster. Datasets are mounted to the pod in advance, and tasks are dispatched and run one at a time. The -n parameter controls the number of concurrent trials. With -n 1 (a concurrency of 1), Trial pods are created one at a time, and the next task starts only after the previous one finishes. For in-cluster scenarios such as a CI/CD pipeline, wrap the harbor run command in a Kubernetes Job. For more information, see Use a Kubernetes Job to orchestrate evaluations.

Prepare the environment

Install the Harbor CLI

Clone the repository from GitHub and install it using pip:

git clone https://github.com/alibaba/harbor.git
cd harbor
pip install -e .
harbor --version

Configure kubeconfig

The Harbor ACK environment uses kubeconfig to connect to the cluster. Run the following command to verify the connection:

kubectl cluster-info

To specify a context or a kubeconfig file, pass the corresponding parameters to the harbor run command:

--ek context=my-ack-context
# or
--ek kubeconfig=/path/to/kubeconfig

Create an evaluation namespace

Use a dedicated namespace to store all evaluation-related resources, such as trial pods, Secrets, and PVs/PVCs:

kubectl create namespace harbor-eval

Other tools (optional)

Mount datasets

Because trial pods are ephemeral, you must mount datasets and result directories from external storage. For production evaluations, using an OSS CSI static volume is recommended. For development and debugging, specify a local directory path directly.

Option 1: OSS CSI static volume (recommended)

Use ACK's OSS CSI plugin to mount a directory from an OSS bucket to a pod. Create two sets of PVs/PVCs: one for the dataset, mounted as read-only, and another for the results, mounted as read-write. Both PVs use the same Secret containing an AccessKey for authentication.

First, create a Secret containing your OSS AccessKey for PV authentication:

kubectl create secret -n harbor-eval generic oss-secret \
  --from-literal=akId=<YOUR-ACCESS-KEY-ID> \
  --from-literal=akSecret=<YOUR-ACCESS-KEY-SECRET>

Dataset volume (read-only):

apiVersion: v1
kind: PersistentVolume
metadata:
  name: harbor-dataset-pv
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain
  csi:
    driver: ossplugin.csi.alibabacloud.com
    volumeHandle: harbor-dataset-pv
    volumeAttributes:
      bucket: my-harbor-bucket
      url: oss-cn-hangzhou-internal.aliyuncs.com
      otherOpts: "-o ro"
      path: "dataset"
    nodePublishSecretRef:
      name: oss-secret
      namespace: harbor-eval
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: harbor-dataset-pvc
  namespace: harbor-eval
spec:
  accessModes:
    - ReadOnlyMany
  resources:
    requests:
      storage: 100Gi
  volumeName: harbor-dataset-pv
  storageClassName: ""

Results volume (writable):

apiVersion: v1
kind: PersistentVolume
metadata:
  name: harbor-results-pv
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  csi:
    driver: ossplugin.csi.alibabacloud.com
    volumeHandle: harbor-results-pv
    volumeAttributes:
      bucket: my-harbor-bucket
      url: oss-cn-hangzhou-internal.aliyuncs.com
      path: "results"
    nodePublishSecretRef:
      name: oss-secret
      namespace: harbor-eval
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: harbor-results-pvc
  namespace: harbor-eval
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Gi
  volumeName: harbor-results-pv
  storageClassName: ""
Note

Writing to an OSS CSI FUSE mount point requires root permissions, so the Job pod's securityContext.runAsUser must be set to 0.

Option 2: Local directory (for development and debugging)

Store your dataset locally and specify the path using the -p parameter. Harbor uses the Kubernetes exec API to upload task files to the trial pod, so no additional configuration is required.

Pre-build images

Each evaluation task has its own environment/Dockerfile and task.toml. If an image is not pre-built, harbor run builds it inside the trial pod. This process is slow and depends on the build environment in the pod. Pre-build your images and push them to ACR instead.

Deploy BuildKit in the cluster

Because clusters typically lack a Docker daemon, deploy BuildKit to serve as a remote build backend:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: buildkitd
  namespace: harbor-eval
spec:
  replicas: 1
  selector:
    matchLabels:
      app: buildkitd
  template:
    metadata:
      labels:
        app: buildkitd
    spec:
      containers:
        - name: buildkitd
          image: moby/buildkit:v0.15.0
          args:
            - --addr
            - tcp://0.0.0.0:1234
            - --config
            - /etc/buildkit/buildkitd.toml
          securityContext:
            privileged: true
          resources:
            requests:
              cpu: 500m
              memory: 1Gi
            limits:
              cpu: "2"
              memory: 4Gi
          volumeMounts:
            - name: buildkitd-config
              mountPath: /etc/buildkit/buildkitd.toml
              subPath: buildkitd.toml
            - name: registry-auth
              mountPath: /registry-auth/
      volumes:
        - name: buildkitd-config
          configMap:
            name: buildkitd-config
        - name: registry-auth
          secret:
            secretName: buildkitd-registry-auth
---
apiVersion: v1
kind: Service
metadata:
  name: buildkit-service
  namespace: harbor-eval
spec:
  type: ClusterIP
  selector:
    app: buildkitd
  ports:
    - port: 1234
      targetPort: 1234

Configure buildkitd authentication and registry mirror

Because BuildKit v0.15.0 does not support registry authentication through CLI parameters, you must configure it in a configuration file. Create a buildkitd.toml ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: buildkitd-config
  namespace: harbor-eval
data:
  buildkitd.toml: |
    [registry."docker.io"]
      mirrors = ["mirror.gcr.io"]

    [registry."<YOUR-ACR-ADDRESS>"]
      config = "/registry-auth/"

In this example, the Docker Hub image source is set to mirror.gcr.io. The Alibaba Cloud registry mirror does not proxy third-party repositories such as swebench, and pulling from them returns an insufficient_scope error.

Create an ACR authentication Secret (in Docker config format):

kubectl create secret generic buildkitd-registry-auth \
  --namespace=harbor-eval \
  --from-file=config.json=$HOME/.docker/config.json

Build and push images in bulk

The harbor admin upload-images command scans all tasks in the dataset, builds the Dockerfile of each task, and pushes the images to ACR:

harbor admin upload-images \
  -t /path/to/dataset \
  -r <YOUR-ACR-ADDRESS> \
  --remote-buildkit tcp://buildkit-service:1234 \
  --update-config \
  --sanitize-image-names \
  --skip-unchanged \
  -n 4

Parameter

Description

-t

Path to the dataset directory

-r

ACR address

--remote-buildkit

BuildKit service address

--update-config

Updates task.toml with the image information after the build is complete.

--sanitize-image-names

Sanitizes image names for ACR compatibility

--skip-unchanged

Skips tasks whose content has not changed, allowing you to resume interrupted jobs

-n

Number of parallel builds

Command execution flow:

  1. Reads the Dockerfile for each task.

  2. Builds the image on the remote BuildKit service by using buildctl.

  3. Pushes the image to ACR.

  4. Writes docker_image, built_content_hash, and image_sha256 back to task.toml.

Note

Docker Hub has a rate limit of 100 anonymous pulls per 6 hours (200 for authenticated users). Large-scale builds can easily trigger this limit, causing a failed to resolve source metadata error. Ensure you have configured the mirror.gcr.io registry mirror as described previously.

With -n 4 parallelism, building 500 tasks takes approximately 2 to 3 hours. Run the command with nohup ... & disown to prevent the process from being terminated if your terminal disconnects.

After the build is complete, verify the image:

crane digest <YOUR-ACR-ADDRESS>/<image-name>:<tag>
docker_image field in task.toml

After upload-images runs, the following fields are added to the [environment] section of task.toml:

[environment]
docker_image = "registry.cn-hangzhou.aliyuncs.com/<ns>/harbor/<task-name>@sha256:..."
built_content_hash = "abc123..."
image_sha256 = "sha256:..."

When harbor run starts, the ACK environment reads docker_image first and pulls the image directly. This takes seconds, whereas building the image in the cluster takes minutes.

Run the evaluation

Configure ACR pull credentials

The trial pod requires authentication to pull pre-built images from ACR. Create a docker-registry Secret:

kubectl create secret docker-registry acr-pull-secret \
  --namespace=harbor-eval \
  --docker-server=registry.cn-hangzhou.aliyuncs.com \
  --docker-username=<username> \
  --docker-password=<password>
Note

Passing --docker-password directly on the command line writes the password to your shell history. Perform this operation in a secure environment or use a credential file instead.

Run evaluation tasks

The following command starts an evaluation in an ACK cluster. Harbor creates a trial pod for each task in the dataset and runs the evaluation. Replace -a and -m with the agent and model you use.

harbor run \
  -p ./my-dataset \
  --env ack \
  -n 1 \
  --ek namespace=harbor-eval \
  --ek registry=registry.cn-hangzhou.aliyuncs.com/<ns>/harbor \
  --ek image_pull_secret=acr-pull-secret \
  -a qwen-coder \
  -m qwen3.7-max \
  --no-delete \
  -y

Common parameters:

Parameter

Description

-p

Path to the local dataset

-d

Dataset registry reference, such as org/dataset@latest

--env ack

Use the ACK environment

-n

Number of concurrent trials. With -n 1, trials run serially.

--ek namespace

Namespace for the trial pods

--ek registry

The ACR address. This can be omitted if task.toml already specifies the docker_image.

--ek image_pull_secret

Name of the Secret used to pull the image

-a

The agent to use

-m

The model to use

--no-delete

Keeps the trial pod after the evaluation is complete for troubleshooting

-y

Skips confirmation prompts

Schedule pods to specific nodes

Use a node selector to schedule trial pods to specific instance types and override resource configurations:

harbor run \
  -p ./my-dataset \
  --env ack \
  -n 1 \
  --ek namespace=harbor-eval \
  --ek image_pull_secret=acr-pull-secret \
  --ek 'node_selector={"node.kubernetes.io/instance-type":"ecs.gn7i-c8g1.2xlarge"}' \
  --override-cpus 4 \
  --override-memory-mb 8192 \
  --override-gpus 1 \
  -a qwen-coder \
  -m qwen3.7-max \
  -y

Filter tasks

Use the following parameters to filter which tasks to run:

-i "task-prefix-*"   # Only run tasks with matching names
-x "skip-*"          # Exclude tasks with matching names
-l 10                # Run a maximum of 10 tasks

Pass environment variables to the agent

Use the --ae parameter to pass environment variables, such as an API Key, to the trial pod for the agent to use when it calls the model:

--ae ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
--ae DASHSCOPE_API_KEY=$DASHSCOPE_API_KEY

Use a YAML configuration file

To manage numerous parameters, use a YAML configuration file:

# eval-config.yaml
dataset: ./my-dataset          # Or a registry reference

agent: qwen-coder
model: qwen3.7-max

environment:
  type: ack
  kwargs:
    namespace: harbor-eval
    registry: registry.cn-hangzhou.aliyuncs.com/<ns>/harbor
    image_pull_secret: acr-pull-secret
    service_account: harbor-eval-sa

n_concurrent_trials: 1

agent_env:
  - name: ANTHROPIC_API_KEY
    value: "${ANTHROPIC_API_KEY}"
harbor run -c eval-config.yaml -y

Use ACS sandbox mode (optional)

Enable sandbox mode to run evaluations on Alibaba Cloud Serverless Container (ACS) or to use OpenKruise SandboxSet to pre-warm pods and accelerate trial startup.

The SandboxClaim mode relies on the OpenKruise CRD (agents.kruise.io/v1alpha1). Before you enable this mode, ensure that OpenKruise is installed in the cluster and that the ServiceAccount has the required CRD permissions (for more information, see Configure RBAC).

Core parameters:

Parameter

Description

--ek use_sandbox_claim=true

Enables SandboxClaim mode. Harbor automatically creates a SandboxSet warm pool.

--ek sandbox_image

Specifies the image used by the SandboxSet (defaults to the task.toml file's docker_image value).

--ek sandboxset_replicas

Size of the warm pool. Recommended to set this to the same value as -n.

--ek claim_timeout

Timeout in seconds to wait for the SandboxClaim to become ready

--ek sandbox_annotations

Adds annotations to the sandbox pod (JSON format)

--ek sandbox_labels

Adds labels to the sandbox pod (JSON format)

--ek sandbox_env_vars

Injects environment variables into the sandbox pod (JSON format)

Example of running on ACS:

harbor run \
  -p ./dataset \
  --env ack \
  -n 4 \
  --ek namespace=harbor-eval \
  --ek image_pull_secret=acr-pull-secret \
  --ek use_sandbox_claim=true \
  --ek sandboxset_replicas=4 \
  --ek 'sandbox_annotations={"k8s.aliyun.com/product-on-demand":"acs"}' \
  -a qwen-coder \
  -m qwen3.7-max \
  -y

To schedule standard trial pods to virtual-kubelet nodes without sandbox mode, use the following parameters:

--ek 'node_selector={"type":"virtual-kubelet"}' \
--ek 'tolerations=[{"key":"virtual-kubelet.io/provider","operator":"Exists"}]'

Orchestrate evaluations with a Kubernetes Job

In a CI/CD pipeline, wrap the harbor run command in a Kubernetes Job to trigger an evaluation from within the cluster:

apiVersion: batch/v1
kind: Job
metadata:
  name: harbor-eval-job
  namespace: harbor-eval
spec:
  parallelism: 1
  completions: 1
  backoffLimit: 0
  template:
    spec:
      serviceAccountName: harbor-eval-sa
      restartPolicy: Never
      securityContext:
        runAsUser: 0
      containers:
        - name: harbor-eval
          image: <harbor-cli-image>
          command:
            - harbor
            - run
            - -p
            - /mnt/oss/dataset
            - --env
            - ack
            - -n
            - "1"
            - --ek
            - namespace=harbor-eval
            - --ek
            - image_pull_secret=acr-pull-secret
            - -a
            - qwen-coder
            - -m
            - qwen3.7-max
            - -o
            - /mnt/oss/results/harbor-eval
            - -y
          env:
            - name: ANTHROPIC_API_KEY
              valueFrom:
                secretKeyRef:
                  name: api-keys
                  key: anthropic-api-key
          volumeMounts:
            - name: dataset
              mountPath: /mnt/oss/dataset
              readOnly: true
            - name: results
              mountPath: /mnt/oss/results
      volumes:
        - name: dataset
          persistentVolumeClaim:
            claimName: harbor-dataset-pvc
            readOnly: true
        - name: results
          persistentVolumeClaim:
            claimName: harbor-results-pvc

Configure RBAC

The Job pod needs permissions to perform operations such as creating trial pods. Grant these permissions through a ServiceAccount:

# harbor-eval-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: harbor-eval-sa
  namespace: harbor-eval
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: harbor-eval-role
  namespace: harbor-eval
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/exec", "pods/log"]
    verbs: ["get", "list", "create", "delete", "watch"]
  - apiGroups: [""]
    resources: ["configmaps", "secrets"]
    verbs: ["get", "list"]
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["get", "list", "create", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: harbor-eval-rolebinding
  namespace: harbor-eval
subjects:
  - kind: ServiceAccount
    name: harbor-eval-sa
    namespace: harbor-eval
roleRef:
  kind: Role
  name: harbor-eval-role
  apiGroup: rbac.authorization.k8s.io
kubectl apply -f harbor-eval-rbac.yaml

When using sandbox mode, you must also add permissions to the preceding Role for sandboxsets, sandboxclaims, and sandboxes to allow get/list/create/delete/watch operations.

View evaluation results

Check the status of trial pods

# View all trial pods
kubectl get pods -n harbor-eval -l app=sandbox

# View the logs of a specific pod
kubectl logs -n harbor-eval <pod-name> --tail=100

Check the Job status

When orchestrating evaluations with a Kubernetes Job, check the execution status and logs of the Job:

kubectl get jobs -n harbor-eval
kubectl describe job harbor-eval-job -n harbor-eval
kubectl logs -l job-name=harbor-eval-job -n harbor-eval --tail=200

View the evaluation result files

The result directory structure for each trial is as follows:

harbor-eval/
└── <task-name>/
    ├── agent/          # Agent execution logs
    ├── verifier/       # Verifier validation results
    ├── artifacts/      # Task artifacts
    └── results.json    # Summary results

When orchestrating a Kubernetes Job, use -o /mnt/oss/results to specify the output path. The results are written directly back to OSS through OSS CSI FUSE, so no additional upload is required. After the evaluation is complete, use ossutil to view the output:

ossutil ls oss://my-harbor-bucket/results/harbor-eval/

FAQ

Pod is stuck in Pending state

Check the pod's events:

kubectl describe pod <pod-name> -n harbor-eval

Common causes include:

  • Insufficient node resources: Adjust --override-cpus or --override-memory-mb.

  • Image pull failed: Check image_pull_secret and the ACR address configuration.

  • Node selector mismatch: Confirm that the labels in the node_selector exist on the cluster nodes.

Image pull failure

# Confirm that the target image exists in ACR
docker manifest inspect <acr-address>/<image>:<tag>

# Confirm that the Secret and the pod are in the same namespace
kubectl get secret acr-pull-secret -n harbor-eval

Harbor CLI cannot connect to the cluster

# Verify the kubeconfig
kubectl cluster-info

# Confirm RBAC permissions from within a pod
kubectl auth can-i create pods -n harbor-eval --as=system:serviceaccount:harbor-eval:harbor-eval-sa

When running in a cluster, the VPC private IP address that the kubeconfig file points to may not be accessible from the pod CIDR. Change the API Server address to the in-cluster Service endpoint: https://kubernetes.default.svc:443.

Evaluation task times out

Task timeout is controlled by the following fields in task.toml:

[agent]
timeout_sec = 300

[verifier]
timeout_sec = 60

For global adjustments, use the top-level CLI parameters (not the --ek parameter):

--agent-timeout-multiplier 2.0

The docker_image field has no effect

Make sure the Harbor CLI version is 0.6.5 or later. Older versions do not read the docker_image field.

harbor --version

Related documents