Skip to main content
A harness is the small program Archal runs instead of your app UI. It reads one task, calls your real agent code, and prints the final answer. npx archal init generates one for you at ./.archal/harness.mjs (run with node, no extra tooling). Prefer TypeScript? Put it at ./.archal/harness.ts and run it with tsx instead - the runner accepts harness.{mjs,js,ts,cjs}.

Template

./.archal/harness.ts
const task = process.env.AGENT_TASK;
if (!task) {
  console.error('Missing AGENT_TASK');
  process.exit(1);
}

// Call your real agent runtime. Do not boot the full app shell.
const result = await runMyAgent({ task });

const text = typeof result === 'string' ? result : JSON.stringify(result);
console.log(JSON.stringify({ text }));
Send logs to stderr. Keep stdout for the final answer.

What Archal reads from stdout

Archal extracts the final answer from stdout in this order:
  1. One JSON object. Archal reads payloads[] or text. Recommended:
    { "text": "The final answer." }
    
  2. JSON object in mixed stdout. Works, but logs can make this brittle.
  3. NDJSON. Useful if your harness streams events.
  4. Plain text. Archal joins non-empty stdout lines outside JSON blocks.
For most harnesses, print exactly one JSON object.

Environment variables

Archal sets these on the harness process before it spawns:
VariableDescription
AGENT_TASKThe full task text (scenario prompt or --task "...").
AGENT_CLONE_URLSJSON map of clone names to REST base URLs for local harnesses.
AGENT_ROUTE_HEADERSJSON headers to include when a local harness calls an AGENT_CLONE_URLS endpoint.
NODE_EXTRA_CA_CERTSPath to the short-lived CA cert when the runtime cannot rely on the system trust store.
Archal does not inject your model keys. Put OPENAI_API_KEY, ANTHROPIC_API_KEY, and similar secrets in .env or CI secrets.

Exit-code contract

CodeMeaning
0Run succeeded, final answer printed to stdout. Evaluator scores the trace.
non-zeroRuntime error. The run is marked failed; [P] criteria are not evaluated.

How to route service traffic

Use the same SDKs, service domains, and normal credential env vars your agent would use in production. For example, use Octokit against api.github.com. Transparently routing unmodified SDKs that call real service domains (like Octokit against api.github.com) requires Docker or sandbox mode, so Archal can control DNS and TLS trust. A harness that instead calls the clone URL from AGENT_CLONE_URLS directly is scored without either (see the minimal example below). For a local harness that calls the clone directly instead of using Docker or sandbox routing, point the SDK at the matching URL in AGENT_CLONE_URLS and merge AGENT_ROUTE_HEADERS into each clone request. Keep the normal service authorization header too, for example Authorization: Bearer <test-token> for GitHub REST.

Minimal clone-calling harness (no agent framework)

If you do not have an agent yet, the fastest path to a first score is a harness that calls the clone’s REST API directly. Archal injects AGENT_CLONE_URLS and AGENT_ROUTE_HEADERS when it starts the run, so the harness needs no Archal SDK. This one matches the scenarios/first-run.md that archal init scaffolds
  • list and summarize GitHub issues, read-only:
./.archal/harness.mjs
// Archal injects AGENT_CLONE_URLS + AGENT_ROUTE_HEADERS when it starts the run.
const task = process.env.AGENT_TASK ?? '';
const cloneUrls = JSON.parse(process.env.AGENT_CLONE_URLS ?? '{}');
const routeHeaders = JSON.parse(process.env.AGENT_ROUTE_HEADERS ?? '{}');

const base = cloneUrls.github; // key matches "clones": ["github"] in .archal.json
if (!base) {
  // Expected when you run this file directly - clone URLs only exist during `archal run`.
  console.log(JSON.stringify({ text: 'No GitHub clone URL was provided.' }));
  process.exit(0);
}

const headers = {
  ...routeHeaders, // x-route-authorization: authenticates YOU to Archal
  // Default GitHub clone bootstrap token (per-service list: /guides/authentication):
  Authorization: 'Bearer ghp_AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTt',
  Accept: 'application/vnd.github+json',
};

// Discover the seeded repos, then list each repo's issues.
const repos = await (await fetch(`${base}/user/repos`, { headers })).json();
const lines = [];
for (const repo of repos) {
  const res = await fetch(`${base}/repos/${repo.full_name}/issues`, { headers });
  for (const issue of await res.json()) {
    lines.push(`#${issue.number} ${issue.title} (${repo.full_name})`);
  }
}

console.log(JSON.stringify({
  text: `Task: ${task}\nOpen issues across ${repos.length} repo(s):\n${lines.join('\n')}`,
}));
Those two headers do different jobs: AGENT_ROUTE_HEADERS (x-route-authorization) authenticates you to Archal, while the normal service Authorization header is what the clone validates - keep both. The default bootstrap token for each service is listed in Authentication. Once you have a real agent, swap this for a call into it and let it use normal SDKs.

Check it locally

Run the entrypoint directly without provisioning clones:
# .mjs (what `archal init` scaffolds), run with node:
AGENT_TASK="Reply with OK and do not use tools." node ./.archal/harness.mjs
# .ts variant, run with tsx:
AGENT_TASK="Reply with OK and do not use tools." npx tsx ./.archal/harness.ts
That should print a final answer and exit 0. (The clone-calling example above prints No GitHub clone URL was provided. here - that is expected; the clone URLs only exist during archal run.) If it hangs or crashes, archal run will too.