can1357/oh-my-pi · error

imageUrls exposure " " requires the binary on PATH

Error message

imageUrls exposure "${name}" requires the ${name} binary on PATH

What it means

requireBinary resolves a tunnel/forward helper binary (cloudflared, ngrok, tailscale, ssh, devtunnel, zrok, bore) via $which and throws when it is not on PATH. Each exposure kind that spawns a child process needs its binary, and the broker refuses to start the exposure without it (the caller then falls back to inline base64 images).

Solutions

  1. Install the required binary (e.g. `brew install cloudflared`, `apt install ngrok`) or download it from the vendor.
  2. Ensure the binary's directory is on PATH for the process running omp (check `echo $PATH` in that environment).
  3. Pick an exposure kind whose binary you already have — e.g. "ssh" if only ssh is installed, or "direct" which needs no binary.
  4. If omp runs as a daemon, export the full PATH in the service unit environment.

Example fix

# before: imageUrls destination "ngrok" without ngrok installed
# after
brew install ngrok   # or apt/scoop equivalent
export PATH="$PATH:/usr/local/bin"
# or switch config to a binary-less kind: "direct"
Defensive patterns

Strategy: validation

Validate before calling

import { $which } from "@oh-my-pi/pi-utils";
const REQUIRED = { cloudflared: "cloudflared", ngrok: "ngrok", tailscale: "tailscale", devtunnel: "devtunnel", zrok: "zrok", bore: "bore", ssh: "ssh" };
if (!REQUIRED[kind] || !$which(REQUIRED[kind])) {
  throw new Error(`exposure "${kind}" needs the ${REQUIRED[kind] ?? kind} binary installed and on PATH`);
}

Try / catch

try {
  await startExposure(config, port);
} catch (err) {
  if (err instanceof Error && err.message.includes("binary on PATH")) {
    logger.warn("tunnel binary missing; images will be inlined as base64", { kind: config.kind });
  } else throw err;
}

Prevention

When it happens

Trigger: Configuring imageUrls exposure kind "cloudflared"/"ngrok"/"tailscale"/"devtunnel"/"zrok"/"bore" (or ssh-based kinds) on a machine where that binary is not installed or not on the PATH of the omp process.

Common situations: Fresh machine or CI container missing tunnel CLIs; binary installed via version manager (homebrew, nvm-style paths) not present in the daemon's PATH; launching omp from a systemd unit or IDE with a minimal PATH.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/6bb667105c768ce1. Report an issue: GitHub.

Appendix: source

Thrown at packages/coding-agent/src/blob-broker/exposure.ts:153

}

/** Public frontend origin printed by `zrok share public`. */
export function parseZrokUrl(line: string): string | null {
	return /https:\/\/[a-z0-9-]+\.share\.zrok\.io/i.exec(line)?.[0] ?? null;
}

/** HTTP origin constructed from the host and port reported by `bore local`. */
export function parseBoreUrl(line: string, fallbackHost?: string): string | null {
	const match = /listening at (?:(?<host>[a-z0-9.-]+):)?(?<port>\d+)/i.exec(line);
	const host = match?.groups?.host ?? fallbackHost;
	const port = match?.groups?.port;
	return host && port ? `http://${host}:${port}` : null;
}

function requireBinary(name: string): string {
	const path = $which(name);
	if (!path) {
		throw new Error(`imageUrls exposure "${name}" requires the ${name} binary on PATH`);
	}
	return path;
}

function normalizeBaseUrl(url: string): string {
	return url.replace(/\/+$/, "");
}

function boundedInteger(value: number | undefined, fallback: number, maximum: number): number {
	if (value === undefined || !Number.isFinite(value)) return fallback;
	return Math.min(maximum, Math.max(0, Math.floor(value)));
}

/**
 * Verify that a public exposure reaches the local blob origin.
 *
 * Each request is cache-busted and time-bounded. Only the broker health
 * endpoint's exact 204 response is accepted; errors expose only the sanitized

View on GitHub (pinned to 9690622007)