Files
openclaw/scripts/e2e/telegram-bot-api-proxy.ts
Ayaan Zaidi 10e0e690df feat(mantis): programmable proof boundaries with recipe library (#127782)
Makes the Mantis Telegram Desktop proof agent programmable at its trust boundaries: declarative Bot API fault rules (drop/status per method), per-request scripted mock-provider responses, observe-until predicates (post-cursor events/text, cumulative provider count), and bounded proxy-side recording of outbound Bot API requests as trusted lane facts. Adds a reusable recipe library under .github/codex/prompts/mantis-recipes/ and raises the proof agent's reasoning effort to high.

Security: the SUT container shadows proxy-control with an inaccessible tmpfs so candidate PR code sharing the mantis-sut uid cannot read or rewrite the proxy's recorded evidence; unmount is blocked by cap-drop/no-new-privileges. Proof doctrine now treats proxy-recorded Bot API facts as trusted comparison evidence and provider request logs as diagnostics.

Follow-up named in PR: move the mock OpenAI server out of the SUT container so provider request facts also become candidate-tamper-proof.
2026-08-22 11:06:06 +05:30

376 lines
12 KiB
JavaScript

#!/usr/bin/env -S node --import tsx
import { appendFileSync, closeSync, fstatSync, openSync, readFileSync } from "node:fs";
import http, { type IncomingMessage, type ServerResponse } from "node:http";
import https from "node:https";
import { pathToFileURL } from "node:url";
const MAX_RECORDED_BODY_BYTES = 32 * 1024;
const MAX_TRUNCATED_PREVIEW_BYTES = 8 * 1024;
const hopByHopHeaders = new Set([
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
]);
function requiredEnv(name: string): string {
const value = process.env[name]?.trim();
if (!value) {
throw new Error(`${name} is required`);
}
return value;
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}
export function rewriteTelegramBotApiPath(
requestPath: string,
aliasToken: string,
upstreamToken: string,
): string | undefined {
const alias = escapeRegExp(aliasToken);
const methodMatch = new RegExp(`^/bot${alias}/([A-Za-z][A-Za-z0-9_]{0,63})(\\?.*)?$`, "u").exec(
requestPath,
);
if (methodMatch) {
return `/bot${upstreamToken}/${methodMatch[1]}${methodMatch[2] ?? ""}`;
}
const fileMatch = new RegExp(`^/file/bot${alias}/([^?#]+)(\\?.*)?$`, "u").exec(requestPath);
const filePath = fileMatch?.[1];
if (filePath && !filePath.split("/").includes("..")) {
return `/file/bot${upstreamToken}/${filePath}${fileMatch[2] ?? ""}`;
}
return undefined;
}
function forwardedHeaders(headers: IncomingMessage["headers"]): http.OutgoingHttpHeaders {
return Object.fromEntries(
Object.entries(headers).filter(
([name, value]) => value !== undefined && !hopByHopHeaders.has(name),
),
);
}
type FaultRule = {
method: string;
mode?: "drop";
status?: number;
times?: number;
};
type FaultControlState = {
remaining: number[];
rules: FaultRule[];
version: string;
};
type RequestBodyCapture = {
contentType?: string;
requestBody?: unknown;
};
function botApiMethod(requestPath: string, aliasToken: string): string | undefined {
const match = new RegExp(
`^/bot${escapeRegExp(aliasToken)}/([A-Za-z][A-Za-z0-9_]{0,63})(?:\\?.*)?$`,
"u",
).exec(requestPath);
return match?.[1];
}
function parseFaultRules(value: unknown): FaultRule[] {
if (!value || typeof value !== "object" || !("rules" in value) || !Array.isArray(value.rules)) {
throw new Error("Telegram proxy control rules are invalid");
}
return value.rules.map((rawRule) => {
if (!rawRule || typeof rawRule !== "object" || Array.isArray(rawRule)) {
throw new Error("Telegram proxy fault rule is invalid");
}
const method = "method" in rawRule ? rawRule.method : undefined;
const times = "times" in rawRule ? rawRule.times : undefined;
const status = "status" in rawRule ? rawRule.status : undefined;
const mode = "mode" in rawRule ? rawRule.mode : undefined;
if (typeof method !== "string" || !/^[A-Za-z][A-Za-z0-9_]{0,63}$/u.test(method)) {
throw new Error("Telegram proxy fault rule method is invalid");
}
if (times !== undefined && (!Number.isInteger(times) || Number(times) < 1)) {
throw new Error("Telegram proxy fault rule times is invalid");
}
if (
status !== undefined &&
(!Number.isInteger(status) || Number(status) < 400 || Number(status) > 599)
) {
throw new Error("Telegram proxy fault rule status is invalid");
}
if (mode !== undefined && mode !== "drop") {
throw new Error("Telegram proxy fault rule mode is invalid");
}
if (mode === "drop" && status !== undefined) {
throw new Error("Telegram proxy fault rule cannot combine status and drop");
}
return {
method,
...(times === undefined ? {} : { times: Number(times) }),
...(status === undefined ? {} : { status: Number(status) }),
...(mode === undefined ? {} : { mode }),
};
});
}
function readControlFile(file: string): { text: string; version: string } {
const descriptor = openSync(file, "r");
try {
const stat = fstatSync(descriptor, { bigint: true });
return {
text: readFileSync(descriptor, "utf8"),
version: `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}`,
};
} finally {
closeSync(descriptor);
}
}
function matchingFaultRule(
controlFile: string,
method: string,
state: { current?: FaultControlState },
): FaultRule | undefined {
// Atomic lane writes replace the inode. Read and version one descriptor so a
// replacement cannot pair new rules with an old finite-use counter.
const { text, version } = readControlFile(controlFile);
if (state.current?.version !== version) {
const rules = parseFaultRules(JSON.parse(text));
state.current = {
remaining: rules.map((rule) => rule.times ?? -1),
rules,
version,
};
}
const current = state.current;
for (let index = 0; index < current.rules.length; index += 1) {
const remaining = current.remaining[index];
if (current.rules[index]?.method !== method || remaining === undefined || remaining === 0) {
continue;
}
if (remaining !== -1) {
current.remaining[index] = remaining - 1;
}
return current.rules[index];
}
return undefined;
}
function boundedBodyMarker(buffer: Buffer, byteLength: number, reason: string): unknown {
return {
__mantisTruncated: true,
byteLength,
preview: buffer.subarray(0, MAX_TRUNCATED_PREVIEW_BYTES).toString("utf8"),
reason,
};
}
function captureRequestBody(request: IncomingMessage): Promise<RequestBodyCapture> {
const contentType = request.headers["content-type"]?.split(";", 1)[0]?.trim().toLowerCase();
const multipart = contentType === "multipart/form-data";
return new Promise((resolve) => {
const chunks: Buffer[] = [];
let capturedBytes = 0;
let byteLength = 0;
const finish = () => {
if (multipart) {
resolve({ contentType: request.headers["content-type"] });
return;
}
const buffer = Buffer.concat(chunks, capturedBytes);
if (byteLength > MAX_RECORDED_BODY_BYTES) {
resolve({ requestBody: boundedBodyMarker(buffer, byteLength, "body exceeds 32 KiB") });
return;
}
if (buffer.length === 0) {
resolve({ requestBody: null });
return;
}
try {
const parsed = JSON.parse(buffer.toString("utf8"));
if (Buffer.byteLength(JSON.stringify(parsed)) > MAX_RECORDED_BODY_BYTES) {
resolve({
requestBody: boundedBodyMarker(buffer, byteLength, "parsed JSON exceeds 32 KiB"),
});
return;
}
resolve({ requestBody: parsed });
} catch {
resolve({ requestBody: boundedBodyMarker(buffer, byteLength, "body is not JSON") });
}
};
let finished = false;
const finishOnce = () => {
if (!finished) {
finished = true;
finish();
}
};
request.on("data", (chunk: Buffer) => {
byteLength += chunk.length;
if (!multipart && capturedBytes < MAX_RECORDED_BODY_BYTES) {
const captured = chunk.subarray(0, MAX_RECORDED_BODY_BYTES - capturedBytes);
chunks.push(captured);
capturedBytes += captured.length;
}
});
request.once("end", finishOnce);
request.once("aborted", finishOnce);
request.once("error", finishOnce);
});
}
function appendRecord(recordFile: string | undefined, entry: Record<string, unknown>): void {
if (recordFile) {
appendFileSync(recordFile, `${JSON.stringify(entry)}\n`);
}
}
export function createTelegramBotApiProxy(params: {
aliasToken: string;
controlFile?: string;
recordFile?: string;
upstreamOrigin?: URL;
upstreamToken: string;
}): http.Server {
const upstreamOrigin = params.upstreamOrigin ?? new URL("https://api.telegram.org");
const transport = upstreamOrigin.protocol === "https:" ? https : http;
const faultState: { current?: FaultControlState } = {};
return http.createServer((request: IncomingMessage, response: ServerResponse) => {
const upstreamPath = rewriteTelegramBotApiPath(
request.url ?? "",
params.aliasToken,
params.upstreamToken,
);
if (!upstreamPath) {
response.writeHead(404).end();
return;
}
const method = botApiMethod(request.url ?? "", params.aliasToken);
const startedAt = Date.now();
let injectedRule: FaultRule | undefined;
try {
injectedRule =
method && params.controlFile
? matchingFaultRule(params.controlFile, method, faultState)
: undefined;
} catch (error) {
response.writeHead(500, { "content-type": "application/json" });
response.end(JSON.stringify({ ok: false, description: "mantis proxy control invalid" }));
console.error(error);
return;
}
const bodyCapture =
method && (params.recordFile || injectedRule)
? captureRequestBody(request)
: Promise.resolve({});
if (method && injectedRule) {
void bodyCapture.then((body) => {
const status = injectedRule.mode === "drop" ? null : (injectedRule.status ?? 500);
try {
appendRecord(params.recordFile, {
at: new Date().toISOString(),
method,
status,
ok: false,
injected: true,
durationMs: Date.now() - startedAt,
...body,
});
} catch (error) {
console.error(error);
request.socket.destroy(error instanceof Error ? error : new Error(String(error)));
return;
}
if (injectedRule.mode === "drop") {
request.socket.destroy();
return;
}
response.writeHead(status ?? 500, { "content-type": "application/json" });
response.end(
JSON.stringify({
ok: false,
error_code: status ?? 500,
description: "mantis injected fault",
}),
);
});
return;
}
let recorded = false;
const record = async (status: number) => {
if (!method || recorded) {
return;
}
recorded = true;
appendRecord(params.recordFile, {
at: new Date().toISOString(),
method,
status,
ok: status >= 200 && status < 300,
injected: false,
durationMs: Date.now() - startedAt,
...(await bodyCapture),
});
};
const upstream = transport.request(
{
headers: { ...forwardedHeaders(request.headers), host: upstreamOrigin.host },
hostname: upstreamOrigin.hostname,
method: request.method,
path: upstreamPath,
port: upstreamOrigin.port || undefined,
protocol: upstreamOrigin.protocol,
},
(upstreamResponse) => {
const status = upstreamResponse.statusCode ?? 502;
response.writeHead(status, forwardedHeaders(upstreamResponse.headers));
upstreamResponse.once("end", () => {
void record(status).catch((error: unknown) => {
console.error(error);
response.destroy(error instanceof Error ? error : new Error(String(error)));
});
});
upstreamResponse.pipe(response);
},
);
upstream.setTimeout(90_000, () => upstream.destroy(new Error("Telegram Bot API timed out")));
upstream.on("error", () => {
void record(502).catch((error: unknown) => console.error(error));
if (!response.headersSent) {
response.writeHead(502);
}
response.end();
});
request.pipe(upstream);
});
}
function main(): void {
const server = createTelegramBotApiProxy({
aliasToken: requiredEnv("TELEGRAM_PROXY_ALIAS_TOKEN"),
controlFile: process.env.TELEGRAM_PROXY_CONTROL?.trim() || undefined,
recordFile: process.env.TELEGRAM_PROXY_RECORD_FILE?.trim() || undefined,
upstreamToken: requiredEnv("TELEGRAM_PROXY_UPSTREAM_TOKEN"),
});
server.listen(8080, "0.0.0.0", () => console.log("Telegram Bot API proxy listening"));
const close = () => server.close();
process.once("SIGINT", close);
process.once("SIGTERM", close);
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main();
}