/** * The interception layer: compression on the wire. * * WHY A PROXY AT ALL, when this package is built out of hooks. Because a hook * provably cannot do this. `docs/superpowers/spikes/2026-08-30-posttooluse- * rewrite.md` established that the `PostToolUse` output schema is * `{hookEventName, additionalContext?, classifierContext?}` and that * `updatedOutput`, `modifiedOutput`, `toolResponse`, `updatedResult` and * `replaceOutput` occur ZERO times in the Claude Code binary. A hook can add * context; it cannot replace a tool result. * * THIS NEVER TRIES TO. It rewrites the outbound REQUEST, and tool results are * already inside that request as conversation history -- so the bytes a hook * cannot touch at PostToolUse are fully rewritable one moment later, here. * That is the entire idea, and it is why this reaches results from other * vendors' MCP servers, WebFetch bodies, and the history that grows every turn, * none of which a hook can see. * * WHAT IT WILL NOT DO: * - bind anywhere but loopback; * - store, log or persist a payload; * - read or retain a credential -- headers are forwarded verbatim; * - rewrite a message carrying signed thinking blocks (a permanent 400); * - touch anything at or before the last cache breakpoint; * - run at all unless explicitly enabled. */ import { type Server } from 'node:http'; import { type TransformationLog } from './transformations.js'; import { type AnchorStore } from '../compress/anchor.js'; import type { SpillSink, Stamp } from '../compress/types.js'; import { type OutputArm } from './output-savings.js'; import type { Finding } from '../compress/knowledge.js'; import { type CompressionOptions, type PresetName, type Tuning } from '../compress/options.js'; import { type AppliedPosture, type PostureName } from './posture.js'; /** * May the default upstream serve this path? * * Exported so the rule can be tested for what it ADMITS as well as what it refuses -- * the refusal is easy to exercise end to end, while proving `/v1/messages` still gets * through would otherwise mean letting a test reach api.anthropic.com. */ export declare function defaultUpstreamServes(path: string): boolean; /** True when the upstream was chosen for us rather than by the operator. */ export declare function upstreamIsDefault(options: ProxyOptions): boolean; export interface ProxyOptions { readonly port?: number; readonly upstream?: string; /** Called with a one-line summary per request. Never receives payloads. */ readonly onSummary?: (summary: ProxySummary) => void; /** * Where to look for this project's knowledge graph. * * Defaults to the directory the proxy was started in, which is where the * rest of this package resolves a project from. */ readonly projectRoot?: string; /** Disable graph injection when a client chooses its working tree after launch. */ readonly knowledge?: boolean; /** Named starting point for the dials. Defaults to the environment's. */ readonly preset?: PresetName | string; /** * Named full-proxy posture: a preset, a set of features, and the dials. * * Defaults to the environment's. A posture SEEDS variables rather than setting * them, so `preset` and `compression` above, and any variable already exported, * all still win -- see proxy/posture.ts. */ readonly posture?: PostureName | string; /** Expert overrides, layered over the preset. */ readonly compression?: CompressionOptions; /** * Largest request body to buffer, in bytes. * * Only ever lowers the built-in ceiling; a larger value is ignored, because raising it * would opt back into the unbounded buffering the ceiling exists to prevent. */ readonly maxBodyBytes?: number; /** * Whether content the engines cannot describe losslessly may leave the * request and be recovered from a file. * * OFF BY DEFAULT, which is a measurement and not caution. An elided body is * a `Read` the agent has to spend a turn on, and a turn is not paid back by * any compression ratio: over the twelve head-to-head workloads, turning * this on wins the cheapest-at-the-first-request column and loses the column * that counts what the agent can answer without going back for anything. * `repeated-reads` is the clearest case -- one file quoted three times costs * 47,613 effective input tokens with this off and nothing to fetch, against * 121,137 for the comparator, and the lossless fold that collapses the * copies is what makes the spill unnecessary rather than merely optional. * * On, it is the right trade on log-and-table shapes where eliding removes * 90% of a block and nothing in the request repeats. That is a caller's call * about its own context pressure, so it is a flag rather than a default. */ readonly spill?: boolean; } export interface ProxySummary { readonly path: string; /** * The model the request names, when it names one. * * See `CompressionFacts.model` in `accounting.ts` for why a size-only record * cannot be turned into money without it, and why an identifier from the * provider's catalog is not request content. */ readonly model?: string; readonly beforeBytes: number; readonly afterBytes: number; readonly compressed: boolean; /** The shaper holdout arm, when one ran. See `CompressionFacts.outputArm`. */ readonly outputArm?: OutputArm; /** * The tool-deferral holdout arm, when one ran. See * `CompressionFacts.deferralArm`. * * DECLARED HERE AND NOT ONLY ON THE FACTS, because the summary is spread into * the ledger record rather than copied field by field -- so a field the * summary type does not admit is one the compiler will never check reached * the ledger, even while it travels there at runtime. */ readonly deferralArm?: OutputArm; readonly reason?: string; /** * Characters of knowledge deliberately ADDED to the request. * * Reported separately from the byte counts because it is the opposite of * what the rest of this does, and a summary that hid it would make the * compression figure beside it a lie. */ readonly injectedChars?: number; /** * Which branch the anchor decision took, and how many elisions the * strategy produced. * * DIAGNOSTIC, AND IT EXISTS BECAUSE INFERENCE FAILED TWICE. Two campaigns * reported 'compression did not pay' on every request, and reading the code * produced two confident explanations that a local reproduction then * disproved. These two fields separate the remaining possibilities without * guessing: no elisions means nothing was touchable or no engine claimed the * content, while elisions with no saving means the rewrite grew the payload. * Neither records any payload. */ readonly anchorReason?: string; readonly elisions?: number; /** Whole units moved out of the body. See CompressionFacts for why it * lives there too and must not be counted by wrapping the sink. */ readonly withheldUnits?: number; /** Section sizes, for locating where a request's bytes live. No content. */ readonly deferredTools?: number; /** Probe: thinking blocks removed from older assistant turns. */ readonly droppedThinking?: number; readonly deferredToolChars?: number; readonly systemChars?: number; readonly toolsChars?: number; readonly toolCount?: number; readonly mcpToolChars?: number; readonly topTools?: string; readonly coreToolChars?: number; readonly messagesChars?: number; readonly messageCount?: number; } /** Enabled by default; explicit opt-outs and the global kill switch win. */ export declare function proxyEnabled(env?: NodeJS.ProcessEnv): boolean; /** The limit in force, which a caller may lower but never raise past the default. */ export declare function bodyLimitFor(options: ProxyOptions): number; /** * A rewritten body, with the keys its markers verify against. * * DELIBERATELY NOT ON `summary`. That object is `ProxySummary` and flows into * the accounting ledger, which carries counts, durations and fixed vocabulary * and nothing derived from the content. A stamp is derived from the content. */ interface CompressedBody { readonly body: Buffer; readonly summary: Omit; readonly stamps: readonly Stamp[]; } /** * Is the proxy required to hand upstream no more bytes than it was given? * * OFF BY DEFAULT, because the cached-knowledge block is a deliberate purchase: * it costs bytes on this request to save tool calls on the next few, and the * trade is usually worth making. But it is a trade, and on a payload that * compresses badly it can leave the wire carrying MORE than the client sent -- * which is a surprising thing for something called a compressing proxy to do, * and a thing anyone measuring us should be able to turn off and compare. */ export declare function netSavingEnabled(env?: NodeJS.ProcessEnv): boolean; /** * Compress a request body, optionally refusing to grow it. * * THE GUARD IS HERE AND NOT AT EACH INJECTION SITE. Knowledge reaches the wire * through three different formats (messages, Responses, Chat Completions), and a * check per site is three places to keep in step; a check on the finished body is * one, and it cannot be bypassed by a fourth format added later. * * The second pass only happens when the first one grew the body, so the ordinary * path pays nothing for this. Dropping `findings` is what makes the retry * smaller -- everything else about the two passes is identical. */ export declare function compressBody(body: Buffer, spill: SpillSink, anchors?: AnchorStore, findings?: readonly Finding[], tuning?: Tuning, sharedGraph?: boolean, wireFormat?: 'chat-completions'): CompressedBody; export declare function knowledgeEnabled(env: NodeJS.ProcessEnv): boolean; /** * How many characters of established knowledge go into the cached prefix. * * A KNOB FOR THE SAME REASON THE TOOL APERTURE IS ONE. The default of 2,000 * fits six to eight lines out of 72,736 characters of eligible claim text -- * 2.7% of what this project has worked out -- and that number was never chosen * against a measurement, only against a worry that a longer block would bury * the relevant lines. The block is charged once as a cache write and then read * at 0.1x, so widening it is cheap per turn and expensive only once; whether it * buys turns is a question for the rig, not for an argument. * * Returns undefined when unset so the preset's own value survives; falling * back to the constant here would silently override a preset that chose a * different budget on purpose. */ export declare function knowledgeCharsFromEnv(env?: NodeJS.ProcessEnv): number | undefined; /** * How many large tool definitions stay loaded when deferral is on. * * A KNOB BECAUSE THE RIGHT VALUE IS MEASURED, NOT REASONED. The default of * five was a guess; the first campaign to run deferral end to end deferred 14 * of 26 definitions, cut the model's context from 29,824 prefix tokens to * 19,138 -- below the 20,431 of an arm with no proxy at all -- and the * transcripts show the tool search tool was never once invoked. Nothing was * searched for, so the guess was too cautious, and finding out how much too * cautious costs a campaign rather than an argument. * * Out-of-range and unparseable values fall back to the default rather than * throwing: a typo in an environment variable must not take a session down. */ export declare function keepToolsFromEnv(env?: NodeJS.ProcessEnv): number; /** * The fraction of conversations deferral is withheld from. None by default. * * A HOLDOUT COSTS REAL MONEY: every conversation in the control arm pays for * tool schemas the treatment arm does not, which is why it is off unless an * operator asks for it and why the computed tier has to stand on its own * without it. */ export declare function deferHoldoutFraction(env?: NodeJS.ProcessEnv): number; /** Deferral defaults on unless the environment explicitly disables it. */ export declare function deferToolsEnabled(env?: NodeJS.ProcessEnv): boolean; /** * Below how many characters a tool definition is exempt from deferral. * The previous default exempted definitions below 1,500 characters to avoid * discovery round trips. Captured traffic showed that 88 of 115 real tools * fell under that floor, leaving about 55 KB undeferred; tool definitions were * 66.9% of the request. That aggregate cost motivated removing the default floor. * * `SMALL_TOOL_CHARS` is now zero: every eligible definition may be deferred. * TOKEN_OPTIMIZER_PROXY_SMALL_TOOL_CHARS can restore a positive exemption for * workloads where discovery round trips cost more than the saved definition. */ export declare function smallToolCharsFromEnv(env?: NodeJS.ProcessEnv): number; /** * Is it safe to send credentials to this upstream? * * CREDENTIALS DO NOT GO OVER CLEARTEXT. Every request through here carries * the user's provider key in a header, forwarded verbatim -- so an upstream * of `http://api.example.com` would put that key on the wire in the clear, * and the override that sets it is a single environment variable. * * Loopback is the exception, and it has to be: the tests in this repository * run a stand-in provider on 127.0.0.1, and so does anyone debugging with a * local recorder. Traffic that never leaves the machine cannot be * intercepted on the way to somewhere else. */ export declare function upstreamIsSafe(upstream: string): boolean; /** * The path and query of a request, and NOTHING a caller can aim with. * * THE UPSTREAM IS OURS TO CHOOSE, NOT THE REQUEST'S. HTTP lets a client send * an absolute-form target -- `POST http://elsewhere.example/v1/messages` -- * and a proxy is exactly the thing that form exists for, so it arrives here * legitimately shaped. `new URL(req.url, upstream)` then IGNORES the base * entirely and resolves to the host in the target, which would send the * user's provider key to whoever asked. A protocol-relative target * (`//elsewhere.example/...`) does the same while keeping our scheme. * * Validating the configured upstream, as `startProxy` does, does not help: * the destination was never read from it. So the request contributes only a * path and a query string, and anything carrying a scheme or an authority is * refused rather than quietly stripped -- a client sending one is either * confused about what this is or is aiming it, and both are worth saying out * loud. */ export declare function requestPath(url: string | undefined): string | null; /** * The bytes the provider will bill as prompt, which are not the bytes we send. * * DEFERRAL DOES NOT REMOVE A SCHEMA FROM THE REQUEST. It marks the schema * `defer_loading: true` and prepends a search tool, and the PROVIDER is what * declines to place a marked schema in context. So the request on the wire is * LARGER than the one we were given while the prompt it becomes is far smaller: * measured on one 40-tool request, 7228 tokens in, 7422 out, 944 in the prompt. * Counting the wire therefore reported -194 where the truth is +6284 -- not an * imprecision but the wrong SIGN, on a feature that is on by default, and the * row was classified an expansion debit and charged against the headline. * * ONE CORRECTION REACHES FOUR SURFACES. `proxyTransportDelta`, * `classifyProxySavings`, `proxyCalibration` and `priceProxyDelta` each read * `afterTokens` and nothing else, so making the prompt the unit of account at * the point of measurement makes the delta, the class, the calibration and the * price prompt-level together, with no fifth place left reading the wire. * * COUNTED, NOT ESTIMATED. We hold the schemas we marked, so the prompt-side * body is constructed exactly rather than modelled. One thing in it is not ours * to count: the search tool we prepend is a two-field stub the provider expands * into a real schema, so our prompt-side count omits bytes the provider bills * and the delta is an UPPER bound, high by that one schema. The deferral * holdout is what measures the net from the provider's own usage. * * BOTH SIDES, because a client may mark its own tools and `deferTools` passes * those through untouched and uncounted. They were never in the client's prompt * either, so leaving them in the before side would credit us with a saving the * provider was already making. * * NULL MEANS DO NOT COUNT THIS ROW. A body that will not parse, or one carrying * no marker although the summary claims a deferral, leaves the two sides * incomparable -- and the wire delta is not a safe fallback, because the wire * delta is exactly the wrong-signed number this exists to replace. An absent * count classifies the row `uncounted`, which is the truth about it. */ export declare function promptSideBody(sent: Buffer, deferredTools?: number): Buffer | null; /** * Starts the proxy. Resolves once it is listening. * * The returned `transformations` log is this listener's own window on what it * has done -- see `transformations.ts` for why it is per listener and why it * holds no payload. A caller that throws the handle away loses only the * ability to answer questions about itself. */ export declare function startProxy(options?: ProxyOptions): Promise<{ server: Server; port: number; transformations: TransformationLog; ledgerSettled: () => Promise; /** * What a named posture did to the environment, or null when none was named. * * RETURNED RATHER THAN PRINTED HERE. The disclosure belongs on the one stream * an operator is watching when they start the proxy, and this function is also * called by the supervisor and by tests, where writing to stderr would be noise * that proves nothing. `proxy/cli.ts` prints it beside the capture notice. */ posture: AppliedPosture | null; }>; export {}; //# sourceMappingURL=server.d.ts.map