mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(release): close the 0.19.0 scan findings — sandbox mongo tag, flow-verb timeout walls, video hardening (#329)
- mongo:8-alpine → mongo:8 (tag never existed; a mongo-opted project could spawn no agents) + a Docker Hub tag-existence e2e guard for every sandbox engine - flow-verb timeouts at both walls: shared SLOW_VERBS policy (i_am_done / submit_up / submit_root / open_pr / i_will_work_on get the 900s server budget); the MCP client now outlasts the server budget (+10s headroom, orchestrator-injected env) so agents receive the middleware's clean 504 envelope instead of dying at the old flat 30s client timeout - cancellation safety: the quality gate kills+reaps its child on CancelledError; create_pr records the PR via a shield-with-wait-out helper so the write can neither be skipped nor race get_db's rollback - video engine: renderer sidecar isolated on a render-only network, 2g/2cpu caps, 570s render watchdog with exit-on-hang, 512MB tar decompression cap, CEO notification on terminal render failure, reject under the approve mutex (fail-closed on Redis-down) - dead python-jose dependency removed (drops ecdsa and its unfixable Minerva advisory PYSEC-2026-1325); panel --font-mono now a real monospace stack Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -20,15 +20,61 @@ const DIMENSIONS = {
|
||||
square: { width: 1080, height: 1080 },
|
||||
};
|
||||
|
||||
// Caps the DECOMPRESSED size (a gzip bomb inflates a tiny upload into a huge
|
||||
// tar stream); MAX_UPLOAD_BYTES in server.js only bounds the compressed
|
||||
// bytes on the wire.
|
||||
const MAX_EXTRACTED_BYTES = Number(
|
||||
process.env.MAX_EXTRACTED_BYTES ?? 512 * 1024 * 1024,
|
||||
);
|
||||
|
||||
/** Thrown when the tar stream's cumulative entry size crosses
|
||||
* MAX_EXTRACTED_BYTES — server.js maps this to a 413. */
|
||||
export class ExtractedSizeExceededError extends Error {
|
||||
constructor(maxBytes) {
|
||||
super(`extracted archive exceeds ${maxBytes} byte cap`);
|
||||
this.name = "ExtractedSizeExceededError";
|
||||
this.statusCode = 413;
|
||||
}
|
||||
}
|
||||
|
||||
async function extractTar(tarBuffer, destDir) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const extractor = tar.extract({ cwd: destDir });
|
||||
let extractedBytes = 0;
|
||||
// ponytail: header-declared entry.size, summed per entry via onentry —
|
||||
// not a byte-exact streaming cap, but tar headers carry the true
|
||||
// (post-gunzip) size, so this catches a bomb before most of it lands.
|
||||
const extractor = tar.extract({
|
||||
cwd: destDir,
|
||||
onentry: (entry) => {
|
||||
extractedBytes += entry.size;
|
||||
if (extractedBytes > MAX_EXTRACTED_BYTES) {
|
||||
extractor.destroy(new ExtractedSizeExceededError(MAX_EXTRACTED_BYTES));
|
||||
}
|
||||
},
|
||||
});
|
||||
extractor.on("finish", resolve);
|
||||
extractor.on("error", reject);
|
||||
Readable.from(tarBuffer).pipe(extractor);
|
||||
});
|
||||
}
|
||||
|
||||
// Deliberately under the orchestrator's 600s client-side HTTP timeout, so
|
||||
// this fires first and the caller gets a clean error instead of an abandoned
|
||||
// connection while Chrome is still wedged server-side.
|
||||
const RENDER_TIMEOUT_SECONDS = Number(
|
||||
process.env.RENDER_TIMEOUT_SECONDS ?? 570,
|
||||
);
|
||||
|
||||
/** Thrown when a render exceeds RENDER_TIMEOUT_SECONDS — server.js maps
|
||||
* this to a 500 and then hard-exits the process (see server.js for why). */
|
||||
export class RenderTimeoutError extends Error {
|
||||
constructor(seconds) {
|
||||
super(`render exceeded ${seconds}s timeout`);
|
||||
this.name = "RenderTimeoutError";
|
||||
this.statusCode = 500;
|
||||
}
|
||||
}
|
||||
|
||||
/** Thrown when the requested orientation's HTML file isn't present in the
|
||||
* composition dir — server.js maps this to a 400 instead of the generic 500
|
||||
* a deep-in-executeRenderJob failure would otherwise produce. The known
|
||||
@@ -115,17 +161,31 @@ export async function renderComposition({
|
||||
height,
|
||||
fps: FPS,
|
||||
});
|
||||
let timer;
|
||||
try {
|
||||
await executeRenderJob(job, (progress) => {
|
||||
console.log(
|
||||
`hyperframes-renderer: ${compositionId}/${orientation} ${Math.round(
|
||||
progress.percent * 100,
|
||||
)}%`,
|
||||
const timeout = new Promise((_resolve, reject) => {
|
||||
timer = setTimeout(
|
||||
() => reject(new RenderTimeoutError(RENDER_TIMEOUT_SECONDS)),
|
||||
RENDER_TIMEOUT_SECONDS * 1000,
|
||||
);
|
||||
});
|
||||
await Promise.race([
|
||||
executeRenderJob(job, (progress) => {
|
||||
console.log(
|
||||
`hyperframes-renderer: ${compositionId}/${orientation} ${Math.round(
|
||||
progress.percent * 100,
|
||||
)}%`,
|
||||
);
|
||||
}),
|
||||
timeout,
|
||||
]);
|
||||
} catch (err) {
|
||||
await rm(outDir, { recursive: true, force: true }).catch(() => {});
|
||||
throw err;
|
||||
} finally {
|
||||
// Clear on both success AND timeout-throw so a completed render never
|
||||
// leaves a dangling timer that fires the watchdog's exit path late.
|
||||
clearTimeout(timer);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user