Harness Integration

The dispatch-time identity pattern

Agent orchestrators (Symphony, lalph, Claude Squad, vibe-kanban, and similar) follow a common model:

  1. Harness dispatches an agent into an isolated workspace
  2. Agent reads a WORKFLOW/SPEC file and calls external services
  3. Results land in an issue tracker or message bus

Ravi fits at step 2 — the agent needs a real email address and phone number to sign up for services, receive OTPs, and send approvals. The challenge: the harness may spin up dozens of workspaces in parallel. Any solution that calls ravi identity use is unsafe — it writes shared global state and will race.

This page shows the safe pattern.


Core technique: RAVI_CONFIG_DIR

Instead of mutating the global identity with ravi identity use, write a workspace-local config and point every Ravi call at it via RAVI_CONFIG_DIR:

# Harness: runs once per workspace, before spawning the agent

# 1. Create a fresh identity for this task
IDENTITY=$(ravi identity create --name "workspace-${TASK_ID}" --json)
IDENTITY_UUID=$(echo "$IDENTITY" | jq -r '.uuid')

# 2. Write a project-scoped config — does NOT touch the global config
mkdir -p /workspace/.ravi
echo "{\"identity_uuid\": \"$IDENTITY_UUID\"}" > /workspace/.ravi/config.json

# 3. Retrieve credentials using the scoped config
export RAVI_EMAIL=$(RAVI_CONFIG_DIR=/workspace/.ravi ravi get email --json | jq -r '.email')
export RAVI_PHONE=$(RAVI_CONFIG_DIR=/workspace/.ravi ravi get phone --json | jq -r '.phone_number')

# 4. Pass credentials to the agent process
exec ravi-agent --email "$RAVI_EMAIL" --phone "$RAVI_PHONE" --workspace /workspace

The agent running inside /workspace inherits RAVI_CONFIG_DIR=/workspace/.ravi and every subsequent ravi call automatically uses workspace-${TASK_ID} — no ravi identity use required.

Parallel safety: Fifty workspaces can run this pattern simultaneously. Each has its own .ravi/config.json. No shared state is written.


Full bash harness example

#!/usr/bin/env bash
set -euo pipefail

TASK_ID="${1:?TASK_ID required}"
WORKSPACE="/tmp/ravi-workspace-${TASK_ID}"

# ── Provision ──────────────────────────────────────────────────────────────────
echo "[harness] creating identity for task ${TASK_ID}"
IDENTITY=$(ravi identity create --name "task-${TASK_ID}" --json)
IDENTITY_UUID=$(echo "$IDENTITY" | jq -r '.uuid')
IDENTITY_EMAIL=$(echo "$IDENTITY" | jq -r '.inbox')
IDENTITY_PHONE=$(echo "$IDENTITY" | jq -r '.phone')

# ── Scope config ───────────────────────────────────────────────────────────────
mkdir -p "${WORKSPACE}/.ravi"
cat > "${WORKSPACE}/.ravi/config.json" <<EOF
{"identity_uuid": "${IDENTITY_UUID}"}
EOF

# ── Verify credentials ─────────────────────────────────────────────────────────
EMAIL=$(RAVI_CONFIG_DIR="${WORKSPACE}/.ravi" ravi get email --json | jq -r '.email')
echo "[harness] identity ready — inbox: ${EMAIL}"

# ── Run agent ─────────────────────────────────────────────────────────────────
(
  export RAVI_CONFIG_DIR="${WORKSPACE}/.ravi"
  export RAVI_EMAIL="${IDENTITY_EMAIL}"
  export RAVI_PHONE="${IDENTITY_PHONE}"
  cd "${WORKSPACE}"
  ./run-agent.sh
)

# ── Cleanup (optional) ────────────────────────────────────────────────────────
echo "[harness] task complete, archiving identity"
# ravi identity archive "task-${TASK_ID}" --json  # when archiving is supported
rm -rf "${WORKSPACE}/.ravi"

Python harness

import subprocess
import json
import os
from pathlib import Path


def provision_workspace(task_id: str, workspace: Path) -> dict:
    """Create a Ravi identity and write a scoped config for the workspace."""
    result = subprocess.run(
        ["ravi", "identity", "create", "--name", f"task-{task_id}", "--json"],
        capture_output=True, text=True, check=True
    )
    identity = json.loads(result.stdout)

    config_dir = workspace / ".ravi"
    config_dir.mkdir(parents=True, exist_ok=True)
    (config_dir / "config.json").write_text(
        json.dumps({"identity_uuid": identity["uuid"]})
    )

    return {
        "uuid": identity["uuid"],
        "email": identity["inbox"],
        "phone": identity["phone"],
        "config_dir": str(config_dir),
    }


def run_agent(task_id: str, workspace: Path):
    credentials = provision_workspace(task_id, workspace)

    env = {
        **os.environ,
        "RAVI_CONFIG_DIR": credentials["config_dir"],
        "RAVI_EMAIL": credentials["email"],
        "RAVI_PHONE": credentials["phone"],
    }

    subprocess.run(
        ["python", "agent.py"],
        cwd=workspace,
        env=env,
        check=True,
    )

Inside agent.py, the agent calls ravi normally — RAVI_CONFIG_DIR ensures every call targets the correct identity with no global mutations.


TypeScript harness

import { execSync } from "child_process";
import * as fs from "fs";
import * as path from "path";

interface RaviIdentity {
  uuid: string;
  inbox: string;
  phone: string;
}

function provisionWorkspace(taskId: string, workspace: string): RaviIdentity & { configDir: string } {
  const raw = execSync(
    `ravi identity create --name "task-${taskId}" --json`,
    { encoding: "utf-8" }
  );
  const identity: RaviIdentity = JSON.parse(raw);

  const configDir = path.join(workspace, ".ravi");
  fs.mkdirSync(configDir, { recursive: true });
  fs.writeFileSync(
    path.join(configDir, "config.json"),
    JSON.stringify({ identity_uuid: identity.uuid })
  );

  return { ...identity, configDir };
}

function runAgent(taskId: string, workspace: string): void {
  const credentials = provisionWorkspace(taskId, workspace);

  const env = {
    ...process.env,
    RAVI_CONFIG_DIR: credentials.configDir,
    RAVI_EMAIL: credentials.inbox,
    RAVI_PHONE: credentials.phone,
  };

  execSync("node agent.js", { cwd: workspace, env, stdio: "inherit" });
}

Environment variable reference

VariableEffect
RAVI_CONFIG_DIROverride config directory (default: ~/.ravi). Set per-process for full isolation.
RAVI_EMAILConvenience — populated by the harness, readable by the agent without a subprocess call.
RAVI_PHONEConvenience — same as RAVI_EMAIL.

The RAVI_CONFIG_DIR override is the safe equivalent of ravi identity use. Prefer it any time more than one agent process is running.


Credential injection without env vars

Some harnesses (Symphony-style WORKFLOW.md front matter, lalph-style .lalph/credentials) pass credentials through a file rather than environment variables. The same RAVI_CONFIG_DIR mechanism applies — just write it into whatever credential format the harness expects:

# Write Ravi credentials into lalph's credential format
cat > "${WORKSPACE}/.lalph/ravi.json" <<EOF
{
  "identity_uuid": "${IDENTITY_UUID}",
  "email": "${IDENTITY_EMAIL}",
  "phone": "${IDENTITY_PHONE}",
  "config_dir": "${WORKSPACE}/.ravi"
}
EOF

Ephemeral identity lifecycle

For task-scoped identities, clean up when the task ends:

# Current: delete the scoped config directory
rm -rf "${WORKSPACE}/.ravi"

# The identity itself remains in Ravi's system until archived
# Name it clearly so you can bulk-archive stale ones:
#   ravi identity list --json | jq '[.[] | select(.name | startswith("task-"))]'

Use a predictable naming prefix (task-, workspace-, ci-) so stale identities are easy to identify and archive in bulk.