Harness Integration
The dispatch-time identity pattern
Agent orchestrators (Symphony, lalph, Claude Squad, vibe-kanban, and similar) follow a common model:
- Harness dispatches an agent into an isolated workspace
- Agent reads a WORKFLOW/SPEC file and calls external services
- 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
| Variable | Effect |
|---|---|
RAVI_CONFIG_DIR | Override config directory (default: ~/.ravi). Set per-process for full isolation. |
RAVI_EMAIL | Convenience — populated by the harness, readable by the agent without a subprocess call. |
RAVI_PHONE | Convenience — same as RAVI_EMAIL. |
The
RAVI_CONFIG_DIRoverride is the safe equivalent ofravi 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.
Related
- Multi-Agent Setup — static per-project isolation with
.ravi/config.json - Production Patterns — Python/TypeScript patterns for retry, polling, and token refresh
- Authentication — parallel agent warning and per-project config detail
- Troubleshooting —
ravi identity userace condition and recovery