{"record":{"id":"6bb667105c768ce1","repo":"can1357/oh-my-pi","slug":"imageurls-exposure-name-requires-the-name","errorCode":null,"errorMessage":"imageUrls exposure \"${name}\" requires the ${name} binary on PATH","messagePattern":"imageUrls exposure \"(.+?)\" requires the (.+?) binary on PATH","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/exposure.ts","lineNumber":153,"sourceCode":"}\n\n/** Public frontend origin printed by `zrok share public`. */\nexport function parseZrokUrl(line: string): string | null {\n\treturn /https:\\/\\/[a-z0-9-]+\\.share\\.zrok\\.io/i.exec(line)?.[0] ?? null;\n}\n\n/** HTTP origin constructed from the host and port reported by `bore local`. */\nexport function parseBoreUrl(line: string, fallbackHost?: string): string | null {\n\tconst match = /listening at (?:(?<host>[a-z0-9.-]+):)?(?<port>\\d+)/i.exec(line);\n\tconst host = match?.groups?.host ?? fallbackHost;\n\tconst port = match?.groups?.port;\n\treturn host && port ? `http://${host}:${port}` : null;\n}\n\nfunction requireBinary(name: string): string {\n\tconst path = $which(name);\n\tif (!path) {\n\t\tthrow new Error(`imageUrls exposure \"${name}\" requires the ${name} binary on PATH`);\n\t}\n\treturn path;\n}\n\nfunction normalizeBaseUrl(url: string): string {\n\treturn url.replace(/\\/+$/, \"\");\n}\n\nfunction boundedInteger(value: number | undefined, fallback: number, maximum: number): number {\n\tif (value === undefined || !Number.isFinite(value)) return fallback;\n\treturn Math.min(maximum, Math.max(0, Math.floor(value)));\n}\n\n/**\n * Verify that a public exposure reaches the local blob origin.\n *\n * Each request is cache-busted and time-bounded. Only the broker health\n * endpoint's exact 204 response is accepted; errors expose only the sanitized","sourceCodeStart":135,"sourceCodeEnd":171,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/exposure.ts#L135-L171","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Install the required binary (e.g. `brew install cloudflared`, `apt install ngrok`) or download it from the vendor.","Ensure the binary's directory is on PATH for the process running omp (check `echo $PATH` in that environment).","Pick an exposure kind whose binary you already have — e.g. \"ssh\" if only ssh is installed, or \"direct\" which needs no binary.","If omp runs as a daemon, export the full PATH in the service unit environment."],"exampleFix":"# before: imageUrls destination \"ngrok\" without ngrok installed\n# after\nbrew install ngrok   # or apt/scoop equivalent\nexport PATH=\"$PATH:/usr/local/bin\"\n# or switch config to a binary-less kind: \"direct\"","handlingStrategy":"validation","validationCode":"import { $which } from \"@oh-my-pi/pi-utils\";\nconst REQUIRED = { cloudflared: \"cloudflared\", ngrok: \"ngrok\", tailscale: \"tailscale\", devtunnel: \"devtunnel\", zrok: \"zrok\", bore: \"bore\", ssh: \"ssh\" };\nif (!REQUIRED[kind] || !$which(REQUIRED[kind])) {\n  throw new Error(`exposure \"${kind}\" needs the ${REQUIRED[kind] ?? kind} binary installed and on PATH`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await startExposure(config, port);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"binary on PATH\")) {\n    logger.warn(\"tunnel binary missing; images will be inlined as base64\", { kind: config.kind });\n  } else throw err;\n}","preventionTips":["Install the tunnel CLI for your chosen destination before enabling it (brew/apt/scoop).","Check $which(binary) at startup and warn early rather than failing mid-session.","For headless/service launches, set an explicit PATH in the unit file or wrapper script.","Prefer the \"direct\" kind in environments where you cannot install binaries."],"tags":["environment","missing-binary","configuration"],"backgroundTag":"binary-not-on-path","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}