Hmbown/CodeWhale · error · Error
Pet Runtime input requires a plain HTTP loopback IP origin…
Error message
Pet Runtime input requires a plain HTTP loopback IP origin, without credentials or a path.
What it means
followRuntime() validates that baseUrl is a plain http:// URL to an IP loopback address (127.0.0.1 or [::1]) with no credentials, no path, query, or hash. This is a deliberate security constraint: pet recordings must never route through non-loopback or credential-bearing endpoints. Any other shape throws before any network activity.
Solutions
- Use `http://127.0.0.1:PORT` (or `http://[::1]:PORT`) with no path, query, credentials, or hash.
- Move the path portion out — the client hits the loopback root only.
- Pass auth via the `token` option, never in the URL.
- If the runtime is remote, run/proxy it locally on loopback; non-loopback origins are unsupported by design.
Example fix
// before
followRuntime({ baseUrl: 'http://localhost:8080/v1', threadId });
// after
followRuntime({ baseUrl: 'http://127.0.0.1:8080', threadId }); Defensive patterns
Strategy: validation
Validate before calling
function assertLoopbackOrigin(baseUrl) {
const u = new URL(baseUrl);
if (u.protocol !== 'http:' || !['127.0.0.1', '[::1]'].includes(u.hostname)
|| u.username || u.password || u.pathname !== '/' || u.search || u.hash)
throw new Error('use a bare http://127.0.0.1[:port] or http://[::1][:port] origin');
} Prevention
- Always use `http://127.0.0.1:PORT`, never `localhost` or https
- Strip paths/queries from copied endpoint URLs
- Pass auth with the token option, not URL credentials
- Remember non-loopback targets are unsupported by design
When it happens
Trigger: Passing `https://127.0.0.1:8080` (https rejected), `http://localhost:8080` (hostname not an IP literal), `http://127.0.0.1:8080/api` (path), `http://user:pass@127.0.0.1` (credentials), or a URL with query/hash.
Common situations: Using `localhost` out of habit instead of `127.0.0.1`; copying a full SDK endpoint URL that includes a path prefix; switching to https for a local dev server; putting an API token into the URL instead of the token option.
Understand the failure class
Background: "Invalid URL" errors: why new URL(), URI.parse, and reqwest::Url reject your string — missing scheme, whitespace, and bad path format — this error's family across 39 libraries.
Related errors
- remote url contains control characters
- reviewed plugin MCP endpoint has an unsafe origin
- Unsupported MCP URL
- append_allow_rules only accepts action = "allow"
- --base-url must use http or https
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/5b9607b04fbabcc4.
Report an issue: GitHub.
Appendix: source
Thrown at pet/scripts/lib/pet-runtime.mjs:12
import { setTimeout as delay } from 'node:timers/promises';
import { privacyEvent, redact } from '../../dist/core/ingest.js';
import { CodewhaleRuntimeTrace, isCodewhaleRuntimeRecord, observeRuntimeRequests } from '../../dist/core/codewhale.js';
/** A read-only transport for the existing Runtime journal. All event meaning
* remains in Whalesong's importer and canonical pet bucketer. No raw journal,
* prompt, tool argument or bearer token is written into the pet recording. */
export async function followRuntime({ baseUrl, threadId, token, report = () => {} }) {
const url = new URL(baseUrl);
if (url.protocol !== 'http:' || !['127.0.0.1', '[::1]'].includes(url.hostname)
|| url.username || url.password || url.pathname !== '/' || url.search || url.hash)
throw new Error('Pet Runtime input requires a plain HTTP loopback IP origin, without credentials or a path.');
if (typeof threadId !== 'string' || !threadId.trim() || threadId.length > 512)
throw new Error('Choose one Runtime --thread ID.');
let sdk;
try { sdk = await import('@codewhale/runtime-sdk'); }
catch { sdk = await import('../../../npm/runtime-sdk/index.js'); }
if (typeof sdk.CodeWhaleRuntimeClient.prototype.threadEvents !== 'function')
throw new Error('The local Runtime SDK needs threadEvents support.');
const client = new sdk.CodeWhaleRuntimeClient({ baseUrl: url.href, token });
const shutdown = new AbortController();
const trace = new CodewhaleRuntimeTrace('Codewhale Runtime', 250_000,
event => privacyEvent(event, 'metadata'), 64 * 1024 * 1024);
let cursor = 0, revision = 0, connected = false, fatal = false;
const done = (async () => {
let backoff = 250;
while (!shutdown.signal.aborted && !fatal) {
// Fifteen-second server heartbeats make a silent, half-open connection
// distinguishable from an idle journal. The timeout is driver time only.
const attempt = new AbortController();View on GitHub (pinned to 433685b202)