{"record":{"id":"5b9607b04fbabcc4","repo":"Hmbown/CodeWhale","slug":"pet-runtime-input-requires-a-plain-http-loopback-ip-origin","errorCode":null,"errorMessage":"Pet Runtime input requires a plain HTTP loopback IP origin, without credentials or a path.","messagePattern":"Pet Runtime input requires a plain HTTP loopback IP origin, without credentials or a path\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"pet/scripts/lib/pet-runtime.mjs","lineNumber":12,"sourceCode":"import { setTimeout as delay } from 'node:timers/promises';\nimport { privacyEvent, redact } from '../../dist/core/ingest.js';\nimport { CodewhaleRuntimeTrace, isCodewhaleRuntimeRecord, observeRuntimeRequests } from '../../dist/core/codewhale.js';\n\n/** A read-only transport for the existing Runtime journal. All event meaning\n * remains in Whalesong's importer and canonical pet bucketer. No raw journal,\n * prompt, tool argument or bearer token is written into the pet recording. */\nexport async function followRuntime({ baseUrl, threadId, token, report = () => {} }) {\n  const url = new URL(baseUrl);\n  if (url.protocol !== 'http:' || !['127.0.0.1', '[::1]'].includes(url.hostname)\n    || url.username || url.password || url.pathname !== '/' || url.search || url.hash)\n    throw new Error('Pet Runtime input requires a plain HTTP loopback IP origin, without credentials or a path.');\n  if (typeof threadId !== 'string' || !threadId.trim() || threadId.length > 512)\n    throw new Error('Choose one Runtime --thread ID.');\n  let sdk;\n  try { sdk = await import('@codewhale/runtime-sdk'); }\n  catch { sdk = await import('../../../npm/runtime-sdk/index.js'); }\n  if (typeof sdk.CodeWhaleRuntimeClient.prototype.threadEvents !== 'function')\n    throw new Error('The local Runtime SDK needs threadEvents support.');\n  const client = new sdk.CodeWhaleRuntimeClient({ baseUrl: url.href, token });\n  const shutdown = new AbortController();\n  const trace = new CodewhaleRuntimeTrace('Codewhale Runtime', 250_000,\n    event => privacyEvent(event, 'metadata'), 64 * 1024 * 1024);\n  let cursor = 0, revision = 0, connected = false, fatal = false;\n  const done = (async () => {\n    let backoff = 250;\n    while (!shutdown.signal.aborted && !fatal) {\n      // Fifteen-second server heartbeats make a silent, half-open connection\n      // distinguishable from an idle journal. The timeout is driver time only.\n      const attempt = new AbortController();","sourceCodeStart":1,"sourceCodeEnd":30,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/pet/scripts/lib/pet-runtime.mjs#L1-L30","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nfollowRuntime({ baseUrl: 'http://localhost:8080/v1', threadId });\n// after\nfollowRuntime({ baseUrl: 'http://127.0.0.1:8080', threadId });","handlingStrategy":"validation","validationCode":"function assertLoopbackOrigin(baseUrl) {\n  const u = new URL(baseUrl);\n  if (u.protocol !== 'http:' || !['127.0.0.1', '[::1]'].includes(u.hostname)\n    || u.username || u.password || u.pathname !== '/' || u.search || u.hash)\n    throw new Error('use a bare http://127.0.0.1[:port] or http://[::1][:port] origin');\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["validation","url","security"],"backgroundTag":"invalid-url-format","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}