/** * Every skill's outbound HTTP door, bounded from ONE declaration. * ============================================================================ * * WHY THIS FILE EXISTS. Node's global `fetch` has NO default timeout, and A * HANG IS NOT A THROW: a connection that is accepted and then never answered * is not an error any `catch` can see — it is a process that stops. Every * provider module in this package is already written for failure (a non-2xx * becomes a worded error the agent can read and route around), and NONE of * that code can run for the one failure mode that actually costs a run. * * MEASURED, not hypothetical: board-runner run 4b49371e (2026-08-24) sat 7m33s * inside the identical unbounded shape until the container watchdog killed it, * and a tick that had already done all of its work recorded nothing. The class * has since been closed one layer at a time — workflow-templates' `lib/kb.js` * (d7e3184), `lib/platform-api.js` (7a355cc), `_shared/tracker.js` (539483e), * `@zibby/core`'s `backend-client.js` (6677c77), `@zibby/agent-workflow`'s * dispatch path (1ef5cca), and the runner's own seven doors (d0d857a, d32ba9c). * * THIS IS THE LAST LAYER, and the widest: ~45 call sites across ~25 provider * modules. That width is exactly why it is a HELPER and not 45 hand-written * `AbortSignal.timeout(...)` calls. Forty-five copies of a clamp, a * `TimeoutError` check and a budget is the TWO-PLACES shape multiplied — a set * that must agree, with nothing to scream when it drifts. One declaration, N * consumers, and `__tests__/skills-http-doors-bounded.test.ts` reads the SOURCE * of every provider module so that call site #46 fails the suite the day it is * WRITTEN rather than the day it hangs somebody's run. * * ⚠️ A SKILL'S HANG IS CHEAPER THAN THE RUNNER'S, WHICH IS WHY IT WAS DEFERRED * AND WHY IT IS NOT FREE. The runner's doors bracket the whole run; a skill's * door costs "only" one tool call. But a tool call the model is waiting on is * a run that is waiting on it, and the container watchdog does not care which * layer stopped. The difference is the BUDGET, not whether there is one. */ export declare const SKILL_API_TIMEOUT_MS = 30000; export declare const SKILL_TRANSFER_TIMEOUT_MS = 120000; export declare const SKILL_JOB_TIMEOUT_MS = 300000; export declare const TIMEOUT_FLOOR_MS = 1000; export declare const TIMEOUT_CEILING_MS = 600000; export type DoorKind = 'api' | 'transfer' | 'job'; /** * Read a budget from the environment, or fall back. Clamped to * [TIMEOUT_FLOOR_MS, TIMEOUT_CEILING_MS]; anything unparseable or non-positive * (`0`, `-1`, `''`, `'soon'`) falls back rather than disabling the bound. */ export declare function timeoutMsFrom(knob: string, fallback: number, env?: any): number; /** The effective budget for a call kind, after the env override and the clamp. */ export declare function timeoutMsForKind(kind?: DoorKind, env?: any): number; /** * `AbortSignal.timeout` aborts with a `TimeoutError` DOMException (undici * rejects the fetch — and any in-flight body read — with that same reason); a * caller-cancelled signal aborts with `AbortError`. Both mean "WE stopped * waiting"; NEITHER means "the far end said no", which is why every call site * has to branch on this before deciding whether to reword an error or rethrow * it UNCHANGED. */ export declare function isTimeoutError(err: any): boolean; /** * The host a URL is aimed at, for the error message. Never the path, never the * query: a presigned S3 URL carries its signature in the query string and a * provider URL carries ids the model will happily paste into a comment, so a * timeout message that echoed the full URL would be a credential leak wearing * a diagnostic's clothes (invariant #4). The host is what a human needs in * order to tell "GitHub is down" from "our control plane is down" from "this * box has no egress", and it is all they need. */ export declare function hostOf(url: any): string; export interface DoorOptions { /** Which budget applies. Defaults to 'api'. */ kind?: DoorKind; /** * What the caller was DOING, in the words a human reading a run log needs: * "GitHub GET /repos/x/y/pulls", "Lark media upload". CLAUDE.md's rule is * that error text must describe THIS failure, not a different era's — a bare * "fetch timed out" makes every one of ~45 doors look identical in a log. */ what?: string; /** An explicit budget, for the rare call that is none of the three kinds. */ timeoutMs?: number; } /** * THE ONE DOOR. A `fetch` that cannot hang, whose timeout says WHAT timed out * and AGAINST WHICH HOST, and which passes a caller's own `signal` straight * through. * * Deliberately a drop-in for `fetch(url, init)`: it returns the same `Response` * and rethrows every non-timeout error as the SAME OBJECT, so a provider * module's existing `!res.ok` handling, its status branches and its error * wording are all untouched. The only observable difference is that the hang * became a throw — which is precisely what every one of those `catch` blocks * was already written for and could never reach. * * ⚠️ THE SIGNAL COVERS THE BODY READ TOO, and that is load-bearing rather than * incidental: a response whose HEADERS arrive and whose BODY then stalls is the * same hang wearing a 200, and bounding only the first half would leave the * door open in the shape hardest to notice. undici rejects an in-flight * `res.json()` / `res.text()` with the request's abort reason, so callers get * the same `TimeoutError` — see `describeTimeout` for turning that half into * words at the call site. */ export declare function fetchWithDeadline(url: any, init?: any, opts?: DoorOptions): Promise; /** * The BODY half, in words. `fetchWithDeadline` cannot wrap `res.json()` for the * caller — the caller decides whether it wants json, text, bytes or nothing — * so a module that reads a body under the same signal wraps the read in this to * get a message in the same voice: * * catch (err) { throw new Error(describeTimeout(err, { what, url, kind, body: true })); } * * Returns `null` when the error is NOT a timeout, so the caller can rethrow the * original object unchanged rather than flattening a real transport error into * a string. */ export declare function describeTimeout(err: any, { what, url, kind, timeoutMs, body }: DoorOptions & { url?: any; body?: boolean; }): string | null;