openclaw/scripts/mantis/telegram-proof-ingress.mts
Martin Cleary a960844a9c
feat(qa): run isolated behavioral proof for inline reviews (#138953)
* feat(telegram): add isolated Test Server proof workflow

Add maintainer-only exact-head admission, durable at-most-once QA lease consumption, isolated candidate execution, and normalized trusted Telegram Test Server observations.

Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>

* feat(proof): bind named Web UI and canonical Telegram QA evidence

Reuse the existing formatting QA recipe, preserve isolated exact-candidate execution and produce the consumer request-bound receipt. Keep named smoke scenarios distinct and protect stationary harness ancestry.

Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>

* fix(proof): complete isolated QA execution and bounded failure capture

Reuse canonical ephemeral device pairing and QA RPC scopes, preserve strict startup probes and recorder locks, and retain bounded wrong-text attempts without Telegram delivery.

Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>

* fix(mantis): revoke proof forwarding and enforce lease roles

* fix(mantis): close proof producer CI gates

* test(mantis): consolidate related proof suites within CI budget

* test(mantis): preserve fast QA ownership when grouping integration cases

* fix(mantis): accept exact branch-qualified workflow paths

* fix(mantis): align live admission workflow path checks

* feat(mantis): collect selected proof inside the originating review

* fix(mantis): bound proof storage and preserve failure evidence

Reuse verified storage across request-bound candidates, reserve backing capacity, retain sanitized rejection evidence, and repair cleanup and observation finalization. Scoped checks and dirty review pass; full Gateway and sandboxed browser runtime proof remain required before publication.

* fix(mantis): retain bridge identity before startup

* fix(mantis): keep candidate config readable under private umask

* fix(qa): use verified rootless networking and join candidate shutdown

* fix(qa): repair proof tooling checks and deterministic recorder fixture

---------

Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>
2026-09-07 14:09:37 +01:00

437 lines
16 KiB
TypeScript

import { appendFile } from "node:fs/promises";
import http from "node:http";
import { z } from "zod";
import { startTelegramTestApiProxy } from "../../.agents/skills/telegram-e2e-userbot/scripts/telegram-test-api-proxy.mjs";
import type { TelegramFailureDiagnostic } from "./request-proof.ts";
import type { TelegramProofPlan } from "./telegram-proof-plan.ts";
const chat = z.union([z.number().int().safe(), z.string().regex(/^-?[1-9][0-9]*$/)]);
const markup = z.strictObject({
inline_keyboard: z
.array(
z
.array(
z.strictObject({
text: z.string().max(128),
callback_data: z.string().max(64),
}),
)
.max(8),
)
.max(8),
});
const send = z.strictObject({
chat_id: chat,
text: z.string().max(4096),
parse_mode: z.enum(["HTML", "Markdown", "MarkdownV2"]).optional(),
link_preview_options: z.strictObject({ is_disabled: z.boolean() }).optional(),
reply_markup: markup.optional(),
reply_parameters: z
.strictObject({
message_id: z.number().int().positive(),
allow_sending_without_reply: z.boolean().optional(),
})
.optional(),
});
const edit = send
.omit({ reply_parameters: true })
.extend({ message_id: z.number().int().positive() });
const remove = z.strictObject({ chat_id: chat, message_id: z.number().int().positive() });
// Profiles are transport metadata, not scenario observations. Keep routing IDs
// and the bot's getMe username, but do not disclose real display names to PR code.
function syntheticProfiles(value: unknown, keepUsername: boolean, depth = 0): unknown {
if (depth > 32) {
throw new Error("Telegram response exceeds projection depth");
}
if (Array.isArray(value)) {
return value.map((item) => syntheticProfiles(item, keepUsername, depth + 1));
}
if (value === null || typeof value !== "object") {
return value;
}
return Object.fromEntries(
Object.entries(value)
.filter(([key]) => key !== "last_name")
.map(([key, item]) => [
key,
key === "first_name"
? "Proof user"
: key === "username" && !keepUsername
? "proof_user"
: syntheticProfiles(item, keepUsername, depth + 1),
]),
);
}
export async function startTelegramProofIngress(options: {
socket: string;
alias: string;
sutToken: string;
testerId: string;
plan: TelegramProofPlan;
providerLog: string;
lease: { assertHealthy(): void; whenUnhealthy: Promise<Error> };
fetchImpl?: typeof fetch;
}) {
type TestApiProxy = Awaited<ReturnType<typeof startTelegramTestApiProxy>>;
const startProxy = startTelegramTestApiProxy as unknown as (_options: {
leaseHealth: typeof options.lease;
fetchImpl: typeof fetch;
}) => Promise<TestApiProxy>;
let closed = false,
invalid = false,
armed = false,
polls = 0,
requests = 0,
writes = 0;
const provider: Array<{ user_text: string; response_text: string; streaming: boolean }> = [];
const messageIds = new Set<number>();
const callbackIds = new Set<string>();
const readers = new Set<AbortController>();
const diagnostics: TelegramFailureDiagnostic[] = [];
const recordDiagnostic = (category: TelegramFailureDiagnostic["category"]) => {
if (diagnostics.length < 16) {
diagnostics.push({ sequence: diagnostics.length + 1, category });
}
};
let stopForwarding!: (error: Error) => void;
const stopped = new Promise<Error>((resolve) => {
stopForwarding = resolve;
});
const cancel = () => {
stopForwarding(new Error("Telegram proof forwarding stopped"));
for (const controller of readers) {
controller.abort();
}
};
const assertHealthy = () => {
if (closed || invalid) {
throw new Error("Telegram ingress is closed or invalid");
}
options.lease.assertHealthy();
};
void options.lease.whenUnhealthy.then(() => {
invalid = true;
cancel();
});
const upstream = await startProxy({
leaseHealth: {
assertHealthy,
whenUnhealthy: Promise.race([options.lease.whenUnhealthy, stopped]),
},
fetchImpl: async (...args) => {
try {
return await (options.fetchImpl ?? fetch)(...args);
} catch (error) {
try {
assertHealthy();
recordDiagnostic("network_failure");
} catch {
recordDiagnostic("authority_unavailable");
}
throw error;
}
},
});
const refuse = (response: http.ServerResponse) => {
if (!response.headersSent) {
response.writeHead(403, { "Content-Type": "application/json" });
}
response.end(JSON.stringify({ ok: false, description: "Outside active Telegram proof scope" }));
};
const server = http.createServer((request, response) => {
let category: TelegramFailureDiagnostic["category"] = "authority_unavailable";
const assertRequestHealthy = () => {
const previous = category;
category = "authority_unavailable";
assertHealthy();
category = previous;
};
const rejectScope: (message: string) => never = (message) => {
category = "scope_rejected";
throw new Error(message);
};
void (async () => {
assertRequestHealthy();
category = "malformed_request";
const probe =
request.method === "GET" &&
[
`/telegram/bot${options.alias}/getMe`,
`/telegram/bot${options.alias}/getWebhookInfo`,
].includes(request.url ?? "");
if (++requests > 512 || (request.method !== "POST" && !probe)) {
rejectScope("Unsupported request");
}
const chunks: Buffer[] = [];
let length = 0;
for await (const chunk of request) {
length += chunk.length;
if (length > 256 * 1024 || (probe && length)) {
throw new Error("Oversized request");
}
chunks.push(Buffer.from(chunk));
}
assertRequestHealthy();
const parsed: unknown = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
const record = z.record(z.string(), z.unknown()).parse(parsed);
if (request.url === "/provider/v1/chat/completions") {
if (!armed || request.headers.authorization !== `Bearer ${options.alias}`) {
rejectScope("Inactive provider capability");
}
const messages = z
.array(z.object({ role: z.string(), content: z.unknown() }))
.max(256)
.parse(record.messages);
const userText = messages
.filter((message) => message.role === "user")
.map((message) =>
typeof message.content === "string" ? message.content : JSON.stringify(message.content),
)
.join("\n");
const reply = options.plan.modelReplies[provider.length];
if (reply === undefined || userText.length > 16384) {
rejectScope("Provider budget exhausted");
}
const entry = {
user_text: userText,
response_text: reply,
streaming: record.stream === true,
};
// Reserve the response before yielding, so concurrent calls cannot reuse it.
provider.push(entry);
await appendFile(options.providerLog, JSON.stringify(entry) + "\n", { mode: 0o600 });
assertRequestHealthy();
if (record.stream === true) {
response.writeHead(200, { "Content-Type": "text/event-stream" });
response.end(
`data: ${JSON.stringify({
id: "mantis-mock",
object: "chat.completion.chunk",
choices: [
{ index: 0, delta: { role: "assistant", content: reply }, finish_reason: null },
],
})}\n\ndata: ${JSON.stringify({
id: "mantis-mock",
object: "chat.completion.chunk",
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
})}\n\ndata: [DONE]\n\n`,
);
} else {
response.writeHead(200, { "Content-Type": "application/json" });
response.end(
JSON.stringify({
id: "mantis-mock",
object: "chat.completion",
choices: [
{ index: 0, message: { role: "assistant", content: reply }, finish_reason: "stop" },
],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
}),
);
}
return;
}
const prefix = `/telegram/bot${options.alias}/`;
if (!request.url?.startsWith(prefix)) {
rejectScope("Wrong Telegram capability");
}
const method = request.url.slice(prefix.length);
// Startup registry operations are simulated; the scenario tests command
// handling, not shared bot registration or webhook administration.
if (["deleteWebhook", "deleteMyCommands", "setMyCommands"].includes(method)) {
response.writeHead(200, { "Content-Type": "application/json" });
response.end(JSON.stringify({ ok: true, result: true }));
return;
}
let outbound: Record<string, unknown>;
if (method === "getMe" || method === "getWebhookInfo") {
outbound = z.strictObject({}).parse(record);
} else if (method === "getUpdates") {
outbound = z
.strictObject({
timeout: z.number().min(0).max(30),
offset: z.number().int().safe().optional(),
limit: z.number().int().min(1).max(100).optional(),
allowed_updates: z.array(z.string().max(64)).max(32).optional(),
})
.parse(record);
} else if (method === "answerCallbackQuery") {
const answer = z
.strictObject({
callback_query_id: z.string().max(256),
text: z.string().max(200).optional(),
show_alert: z.boolean().optional(),
cache_time: z.literal(0).optional(),
})
.parse(record);
if (!armed || ++writes > 64 || !callbackIds.delete(answer.callback_query_id)) {
rejectScope("Callback outside this proof");
}
outbound = answer;
} else {
if (!armed || ++writes > 64 || String(record.chat_id) !== options.testerId) {
rejectScope("Outside leased DM budget");
}
if (method === "sendMessage") {
outbound = { ...send.parse(record), link_preview_options: { is_disabled: true } };
} else if (method === "editMessageText") {
outbound = { ...edit.parse(record), link_preview_options: { is_disabled: true } };
} else if (method === "deleteMessage") {
outbound = remove.parse(record);
} else if (method === "sendChatAction") {
outbound = z.strictObject({ chat_id: chat, action: z.literal("typing") }).parse(record);
} else {
rejectScope("Method outside the selected DM observation surface");
}
if ("message_id" in outbound && !messageIds.has(Number(outbound.message_id))) {
rejectScope("Mutation targets a message outside this proof");
}
if ("reply_parameters" in outbound) {
const parameters = outbound.reply_parameters as { message_id: number };
if (!messageIds.has(parameters.message_id)) {
rejectScope("Reply targets stale message");
}
}
}
const controller = new AbortController();
readers.add(controller);
try {
assertRequestHealthy();
const upstreamUrl = new URL(upstream.apiRoot);
upstreamUrl.pathname = `/bot${options.sutToken}/${method}`;
category = "network_failure";
const result = await fetch(upstreamUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(outbound),
redirect: "error",
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(40_000)]),
});
category = "upstream_failure";
if (!result.ok) {
throw new Error("Upstream request failed");
}
const upstreamChunks: Uint8Array[] = [];
let size = 0;
if (!result.body) {
throw new Error("Missing upstream body");
}
for await (const chunk of result.body) {
size += chunk.length;
if (size > 2 * 1024 * 1024) {
throw new Error("Oversized upstream response");
}
upstreamChunks.push(chunk);
}
const data = z
.record(z.string(), z.unknown())
.parse(JSON.parse(Buffer.concat(upstreamChunks).toString("utf8")));
if (data.ok !== true) {
throw new Error("Upstream result failed");
}
assertRequestHealthy();
if (method === "getUpdates") {
polls += 1;
const updates = z.array(z.record(z.string(), z.unknown())).parse(data.result);
data.result = updates.filter((update) => {
const message = z
.object({
message_id: z.number().int().positive(),
chat: z.object({ id: z.number(), type: z.literal("private") }),
from: z.object({ id: z.number() }),
})
.safeParse(update.message);
if (
message.success &&
String(message.data.chat.id) === options.testerId &&
String(message.data.from.id) === options.testerId
) {
if (armed) {
messageIds.add(message.data.message_id);
}
return armed;
}
const callback = z
.object({
id: z.string().max(256),
from: z.object({ id: z.number() }),
message: z.object({
message_id: z.number().int(),
chat: z.object({ id: z.number(), type: z.literal("private") }),
}),
})
.safeParse(update.callback_query);
const accepted =
armed &&
callback.success &&
String(callback.data.from.id) === options.testerId &&
String(callback.data.message.chat.id) === options.testerId &&
messageIds.has(callback.data.message.message_id);
if (accepted && callback.success) {
callbackIds.add(callback.data.id);
}
return accepted;
});
}
if (method === "sendMessage") {
const sent = z
.object({
message_id: z.number().int().positive(),
chat: z.object({ id: z.number(), type: z.literal("private") }),
})
.parse(data.result);
if (String(sent.chat.id) !== options.testerId) {
throw new Error("Unexpected destination");
}
messageIds.add(sent.message_id);
}
response.writeHead(result.status, { "Content-Type": "application/json" });
response.end(
JSON.stringify(syntheticProfiles(data, method === "getMe")).replaceAll(
options.sutToken,
"[redacted]",
),
);
} finally {
readers.delete(controller);
}
})().catch(() => {
recordDiagnostic(category);
invalid = true;
cancel();
refuse(response);
});
});
server.requestTimeout = 45_000;
server.headersTimeout = 10_000;
await new Promise<void>((resolve, reject) => {
server.once("error", reject);
server.listen(options.socket, resolve);
});
return {
getDiagnostics: () =>
diagnostics.map((entry) => ({ sequence: entry.sequence, category: entry.category })),
assertHealthy,
drainStaleUpdates: () => upstream.drainUpdates(options.sutToken),
isPolling: () => polls > 0,
armScenario() {
assertHealthy();
if (armed) {
throw new Error("Scenario already armed");
}
armed = true;
},
providerCapture: () => provider,
async close() {
closed = true;
cancel();
server.closeAllConnections();
await new Promise<void>((resolve) => {
server.close(() => resolve());
});
await upstream.close();
},
};
}