@opengeni/runtime 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -77,6 +77,19 @@ const SCROLL_MAX_CLICKS = 15;
77
77
  // short to ride out a cold gVisor XFCE boot, so the turn failed loud on a transient.
78
78
  const SCREENSHOT_WARMUP_BUDGET_MS = 30_000;
79
79
  const SCREENSHOT_RETRY_DELAY_MS = 750;
80
+ // The screenshot PNG is read back off the box by base64-ing it over the provider's exec
81
+ // channel. Modal's exec collects stdout into a BOUNDED buffer that silently EMPTIES a
82
+ // single oversized read: a fully-painted 1280x800 desktop PNG (~222 KB) base64s to
83
+ // ~296 KB, which trips the cap and returns "" — the blank-frame incident. A partly-
84
+ // painted frame (~200 KB base64) slipped under it, which is why this only surfaced once
85
+ // the paint gate began delivering FULLY-painted frames. (Channel-A's file read already
86
+ // caps its own base64-over-exec output for the same reason; the screenshot read was the
87
+ // one large read that didn't bound itself.) We therefore read the file in 3-byte-aligned
88
+ // CHUNKS whose per-read base64 (~131 KB) stays well under the cap, then VALIDATE the
89
+ // reconstructed byte count against the on-box file size. 98304 = 96 KiB, divisible by 3
90
+ // so each chunk's base64 is self-contained (no cross-chunk padding) and concatenating the
91
+ // decoded chunks reconstructs the PNG byte-exactly.
92
+ const SCREENSHOT_READ_CHUNK_BYTES = 98_304;
80
93
 
81
94
  export type SandboxComputerOptions = {
82
95
  display?: string; // ":0"
@@ -116,7 +129,9 @@ const BUTTON_NUM: Record<ComputerButton, number> = { left: 1, wheel: 2, right: 3
116
129
  // exec/execCommand (readFile path-validates against /workspace and rejects /tmp).
117
130
  type ExecResultLike = { output?: string; stdout?: string; stderr?: string; exitCode?: number | null; sessionId?: number };
118
131
  type ComputerSession = {
119
- exec?: (args: { cmd: string; runAs?: string; yieldTimeMs?: number; maxOutputTokens?: number }) => Promise<ExecResultLike>;
132
+ // A native provider exec returns a structured `ExecResultLike`; the routing proxy
133
+ // fronting a Modal box returns the execCommand banner STRING. Both are handled.
134
+ exec?: (args: { cmd: string; runAs?: string; yieldTimeMs?: number; maxOutputTokens?: number }) => Promise<ExecResultLike | string>;
120
135
  execCommand?: (args: { cmd: string; runAs?: string; yieldTimeMs?: number; maxOutputTokens?: number }) => Promise<string>;
121
136
  };
122
137
 
@@ -124,6 +139,13 @@ type ComputerSession = {
124
139
  export class ComputerUnavailableError extends Error {
125
140
  constructor(message: string) { super(message); this.name = "ComputerUnavailableError"; }
126
141
  }
142
+ /** The screenshot file read back SHORT of its on-box byte size — a chunk was truncated
143
+ * by the provider's exec-output cap. This is deterministic, NOT a warm-up transient, so
144
+ * it fails LOUD immediately with the byte counts (never a silent blank the model would
145
+ * read as a real empty screen) and is not retried. */
146
+ export class ScreenshotReadError extends Error {
147
+ constructor(message: string) { super(message); this.name = "ScreenshotReadError"; }
148
+ }
127
149
  /** A write action attempted while readOnly. */
128
150
  export class ComputerReadOnlyError extends Error {
129
151
  constructor() { super("computer-use is read-only — write actions are disabled"); this.name = "ComputerReadOnlyError"; }
@@ -268,6 +290,12 @@ export class SandboxComputer implements Computer {
268
290
  return Buffer.from(bytes).toString("base64");
269
291
  } catch (error) {
270
292
  lastError = error;
293
+ // A short/truncated read is deterministic (the exec-output cap), not a warm-up
294
+ // transient — retrying only burns the budget and delays a legible failure. Fail
295
+ // loud NOW (the `finally` still cleans up the temp file).
296
+ if (error instanceof ScreenshotReadError) {
297
+ throw error;
298
+ }
271
299
  } finally {
272
300
  // Best-effort cleanup on every attempt (success OR failure); never mask the
273
301
  // screenshot result.
@@ -288,37 +316,75 @@ export class SandboxComputer implements Computer {
288
316
  throw new ComputerUnavailableError("scrot produced an empty screenshot (display not up?)");
289
317
  }
290
318
 
291
- // Read the screenshot PNG bytes by base64-ing the absolute /tmp path through the
292
- // SAME command primitive (exec ?? execCommand) — NOT `session.readFile` (Modal
319
+ // Run a read-only command over the SAME command primitive computer actions use
320
+ // (exec ?? execCommand) and return its RAW stdout body — NOT `session.readFile` (Modal
293
321
  // path-validates against /workspace and rejects /tmp) and NOT `this.x()` (its
294
- // `sandboxCommandOutput` parser drops the execCommand STRING body, returning ""
295
- // only the exec-object path has a structured body). We capture the RAW result,
296
- // strip the execCommand banner ("…Output:\n<base64>"), strip whitespace, and
297
- // decode. Binary-safe: base64 of the scrot is plain ASCII over stdout, no
298
- // truncation (maxOutputTokens:null), mirroring recording.ts readRecordingBytes.
299
- private async readScreenshotBytes(path: string): Promise<Uint8Array> {
322
+ // `sandboxCommandOutput` parser drops the execCommand STRING body). exec exposes a
323
+ // structured stdout; execCommand returns the formatted STRING, so we strip its banner
324
+ // ("…Output:\n<body>") to recover the body.
325
+ //
326
+ // ROUTING-PROXY SEAM: when the selfhosted feature is on, the turn's box is wrapped in a
327
+ // `RoutingSandboxSession`, which ALWAYS exposes an `exec` method — but for a Modal-backed
328
+ // box (no native `exec`) that method internally falls back to `execCommand` and returns
329
+ // the formatted STRING, not a `{output}` object. `sandboxCommandOutput` returns "" for a
330
+ // string, so a naive `sandboxCommandOutput(await session.exec())` silently dropped the
331
+ // whole screenshot body → empty read → "display not up" error card on EVERY Modal
332
+ // computer-use turn once routing was enabled. So a STRING exec result is banner-stripped
333
+ // exactly like the direct execCommand path; only a structured object goes to
334
+ // sandboxCommandOutput.
335
+ private async readCmdRaw(cmd: string): Promise<string> {
300
336
  const args = {
301
- cmd: `DISPLAY=${this.display} base64 ${path}`,
337
+ cmd,
302
338
  ...(this.runAs ? { runAs: this.runAs } : {}),
303
339
  yieldTimeMs: ACTION_YIELD_MS,
304
- // null disables the provider's output truncation so a full-screen PNG's
305
- // base64 is never clipped (the SDK's truncateOutput passes through on null).
340
+ // null disables the provider's TOKEN truncation; the byte cap this method chunks
341
+ // around is a separate, lower-level exec-stdout buffer limit.
306
342
  maxOutputTokens: null as unknown as number,
307
343
  };
308
- let raw: string;
309
344
  if (typeof this.session.exec === "function") {
310
- // The exec-object path exposes a structured stdout/output body.
311
- raw = sandboxCommandOutput(await this.session.exec(args));
312
- } else if (typeof this.session.execCommand === "function") {
313
- // execCommand returns the formatted STRING — strip the banner to recover the
314
- // base64 body (sandboxCommandOutput would drop it for the string form).
315
- raw = stripExecBanner(await this.session.execCommand(args));
316
- } else {
317
- throw new ComputerUnavailableError("session cannot run commands (no exec/execCommand) — screenshots unavailable");
345
+ const result = await this.session.exec(args);
346
+ // A routing proxy fronting a Modal box returns the execCommand banner STRING;
347
+ // a native structured exec returns an object. Handle both so neither silently
348
+ // yields an empty body.
349
+ return typeof result === "string" ? stripExecBanner(result) : sandboxCommandOutput(result);
350
+ }
351
+ if (typeof this.session.execCommand === "function") {
352
+ return stripExecBanner(await this.session.execCommand(args));
353
+ }
354
+ throw new ComputerUnavailableError("session cannot run commands (no exec/execCommand) — screenshots unavailable");
355
+ }
356
+
357
+ // Read the screenshot PNG bytes off the box. A SINGLE `base64 <file>` over Modal's
358
+ // exec silently returns "" once the base64 exceeds the exec-stdout buffer cap (a full
359
+ // desktop ~296 KB base64 trips it — the blank-frame incident). So we size the file
360
+ // first, then base64 it in SCREENSHOT_READ_CHUNK_BYTES-sized, 3-byte-aligned chunks
361
+ // (each read's base64 stays well under the cap) and reconstruct — validating the total
362
+ // against the on-box size and failing LOUD on any short read rather than handing back a
363
+ // truncated/blank frame. Binary-safe: base64 is plain ASCII over stdout.
364
+ private async readScreenshotBytes(path: string): Promise<Uint8Array> {
365
+ // On-box byte size (tiny output — always safe). A still-warming :0 can yield a
366
+ // zero-byte scrot file; report empty so the caller retries within its warm-up budget.
367
+ const sizeRaw = (await this.readCmdRaw(`wc -c < ${path} 2>/dev/null`)).replace(/[^0-9]/g, "");
368
+ const fileSize = Number.parseInt(sizeRaw, 10);
369
+ if (!Number.isFinite(fileSize) || fileSize <= 0) return new Uint8Array();
370
+
371
+ const chunks: Buffer[] = [];
372
+ const nChunks = Math.ceil(fileSize / SCREENSHOT_READ_CHUNK_BYTES);
373
+ for (let i = 0; i < nChunks; i++) {
374
+ // `dd bs=CHUNK skip=i count=1` reads bytes [i*CHUNK, (i+1)*CHUNK); CHUNK % 3 == 0
375
+ // so each chunk's base64 is self-contained and decoded chunks concatenate exactly.
376
+ const raw = await this.readCmdRaw(
377
+ `dd if=${path} bs=${SCREENSHOT_READ_CHUNK_BYTES} skip=${i} count=1 2>/dev/null | base64`,
378
+ );
379
+ chunks.push(Buffer.from(raw.replace(/\s+/g, ""), "base64"));
380
+ }
381
+ const bytes = Buffer.concat(chunks);
382
+ if (bytes.length !== fileSize) {
383
+ throw new ScreenshotReadError(
384
+ `screenshot read reconstructed ${bytes.length}B of ${fileSize}B (${nChunks} chunk(s), path=${path}) — a chunk was truncated by the exec-output cap; frame incomplete`,
385
+ );
318
386
  }
319
- const b64 = raw.replace(/\s+/g, "");
320
- if (b64.length === 0) return new Uint8Array();
321
- return Uint8Array.from(Buffer.from(b64, "base64"));
387
+ return new Uint8Array(bytes);
322
388
  }
323
389
 
324
390
  async click(xp: number, yp: number, button: ComputerButton) {
@@ -385,7 +451,11 @@ export class SandboxComputer implements Computer {
385
451
  * leaf; the duck-typed `isNativeDesktopSession` probe (below) selects on it. */
386
452
  export type NativeDesktopSession = {
387
453
  desktopInput(event: DesktopInputRequest["event"]): Promise<void>;
388
- screenshot(): Promise<{ png: Uint8Array; width: number; height: number }>;
454
+ // `nativeWidth`/`nativeHeight` are the PRE-DOWNSCALE capture geometry (equal to
455
+ // width/height when the agent did not have to shrink the PNG to fit the transport
456
+ // budget). NativeDesktopComputer scales model clicks from the ENCODED pixel space
457
+ // back to native pixels using their ratio before injecting.
458
+ screenshot(): Promise<{ png: Uint8Array; width: number; height: number; nativeWidth: number; nativeHeight: number }>;
389
459
  };
390
460
 
391
461
  /** Model `Button` → wire `PointerButton`. The proto has no back/forward button, so
@@ -399,12 +469,35 @@ const POINTER_BUTTON: Record<ComputerButton, PointerButton> = {
399
469
  forward: PointerButton.POINTER_BUTTON_UNSPECIFIED,
400
470
  };
401
471
 
472
+ // Native capture (ScreenCaptureKit / x11) can hand back a null/empty FIRST frame in
473
+ // the moments right after the agent connects — the capture stream still warming — the
474
+ // same class of transient the scrot path retries around for a cold :0. Native capture
475
+ // is normally sub-second, so the warm-up budget is much shorter than the scrot path's
476
+ // 30 s cold-Modal-box budget; enough to ride a first-frame miss, not enough to hang a
477
+ // turn on a genuinely dead capture. A TERMINAL failure (permission denied) short-
478
+ // circuits the budget and fails immediately.
479
+ const NATIVE_SCREENSHOT_WARMUP_BUDGET_MS = 6_000;
480
+ const NATIVE_SCREENSHOT_RETRY_DELAY_MS = 400;
481
+
402
482
  export type NativeDesktopComputerOptions = {
403
483
  dimensions?: [number, number]; // the display geometry (must match the capture size)
404
484
  environment?: NonNullable<Computer["environment"]>; // "ubuntu" (default) | "mac" | ...; model uses it for OS key conventions
405
485
  readOnly?: boolean; // when true, every WRITE action throws ComputerReadOnlyError
486
+ screenshotWarmupBudgetMs?: number; // wall-clock budget to retry a warming/empty first frame
487
+ screenshotRetryDelayMs?: number; // delay between warm-up retries
406
488
  };
407
489
 
490
+ /** A capture failure that RETRYING cannot cure — Screen Recording (TCC) has not been
491
+ * granted to the agent, so every capture will deny until the user grants it. We fail
492
+ * FAST on this rather than burn the warm-up budget, and surface the reason verbatim so
493
+ * the operator sees "grant Screen Recording", not a blank screen. Matches the agent's
494
+ * `capture_rgba` denial strings ("Screen Recording permission is not granted",
495
+ * "no shareable content (Screen Recording denied?)"). */
496
+ function isTerminalCaptureDenial(error: unknown): boolean {
497
+ const msg = (error instanceof Error ? error.message : String(error ?? "")).toLowerCase();
498
+ return msg.includes("screen recording") || msg.includes("permission is not granted") || msg.includes("consent");
499
+ }
500
+
408
501
  /**
409
502
  * A `Computer` that drives a SELF-HOSTED machine's OWN desktop NATIVELY over the
410
503
  * control plane (`desktopInput` inject + `screenshot` capture on the bound
@@ -424,6 +517,17 @@ export class NativeDesktopComputer implements Computer {
424
517
  readonly dimensions: [number, number];
425
518
  private session: NativeDesktopSession;
426
519
  private readonly readOnly: boolean;
520
+ private readonly screenshotWarmupBudgetMs: number;
521
+ private readonly screenshotRetryDelayMs: number;
522
+ // The ENCODED vs NATIVE geometry of the MOST RECENT screenshot the model saw. The
523
+ // model computes click coordinates in the encoded-pixel space of that screenshot;
524
+ // when the agent downscaled the PNG to fit the transport budget, encoded < native,
525
+ // so we scale coordinates back up to native pixels before injecting (the agent's
526
+ // native inject — macOS CGEvent / Linux XTEST — expects native-pixel coordinates,
527
+ // exactly as it received them pre-downscale). Null until the first screenshot;
528
+ // equal encoded==native (or absent) ⇒ scale factor 1.0 ⇒ byte-identical behavior.
529
+ private lastEncoded: [number, number] | null = null;
530
+ private lastNative: [number, number] | null = null;
427
531
 
428
532
  constructor(session: NativeDesktopSession, opts: NativeDesktopComputerOptions = {}) {
429
533
  this.session = session;
@@ -432,6 +536,8 @@ export class NativeDesktopComputer implements Computer {
432
536
  // should pass "mac" so the model uses ⌘-based shortcuts — see the coordinate TODO.
433
537
  this.environment = opts.environment ?? "ubuntu";
434
538
  this.readOnly = opts.readOnly ?? false;
539
+ this.screenshotWarmupBudgetMs = opts.screenshotWarmupBudgetMs ?? NATIVE_SCREENSHOT_WARMUP_BUDGET_MS;
540
+ this.screenshotRetryDelayMs = opts.screenshotRetryDelayMs ?? NATIVE_SCREENSHOT_RETRY_DELAY_MS;
435
541
  }
436
542
 
437
543
  /** Rebind to a freshly resumed-by-id session after a box rollover / re-establish. */
@@ -441,31 +547,90 @@ export class NativeDesktopComputer implements Computer {
441
547
  if (this.readOnly) throw new ComputerReadOnlyError();
442
548
  }
443
549
 
550
+ /** Scale a coordinate the model expressed in the MOST RECENT screenshot's
551
+ * ENCODED pixel space back to NATIVE pixels. When the last frame was not
552
+ * downscaled (encoded == native), or no screenshot has been taken yet, this is a
553
+ * 1:1 identity — the byte-identical current behavior. The agent then applies its
554
+ * own platform mapping (macOS divides native pixels by the backing scale to reach
555
+ * CGEvent points; Linux XTEST is 1:1) exactly as it did pre-downscale. */
556
+ private toNative(x: number, y: number): { x: number; y: number } {
557
+ const enc = this.lastEncoded;
558
+ const nat = this.lastNative;
559
+ if (!enc || !nat || enc[0] <= 0 || enc[1] <= 0) return { x, y };
560
+ if (enc[0] === nat[0] && enc[1] === nat[1]) return { x, y };
561
+ return {
562
+ x: Math.round((x * nat[0]) / enc[0]),
563
+ y: Math.round((y * nat[1]) / enc[1]),
564
+ };
565
+ }
566
+
444
567
  private async pointer(x: number, y: number, action: PointerAction, button: PointerButton): Promise<void> {
445
- // COORDINATE SEAM — TODO(verify e2e on macOS): the model computes x/y against the
446
- // pixels of the screenshot it just saw, and the agent's macOS CGEvent inject
447
- // treats x/y as raw screen coordinates. On a Retina Mac, ScreenCaptureKit may
448
- // capture at the logical POINT space while CGEvent expects logical points — a
449
- // potential mismatch between the coords the model derives and the coords the
450
- // inject applies. This MUST be measured on a real Retina Mac (compare the
451
- // screenshot's reported width/height against the logical display bounds) before
452
- // any DPR scaling is added. Do NOT add scaling speculatively. Self-hosted Linux
453
- // (XTEST/x11) is 1:1 and unaffected.
454
- await this.session.desktopInput({ $case: "pointer", pointer: { x, y, action, button } });
568
+ // COORDINATE SEAM: the model computes x/y against the pixels of the screenshot it
569
+ // just saw — which the agent may have DOWNSCALED to fit the transport's max
570
+ // payload (a full-res Retina/busy screen exceeds NATS's 1 MiB default). We scale
571
+ // those encoded-pixel coordinates back to native pixels here, using the native
572
+ // geometry the last screenshot reported, so the agent's native inject lands the
573
+ // click where the model intended. When no downscale occurred the factor is 1.0
574
+ // and the coordinates pass through unchanged. Self-hosted Linux (XTEST/x11) and
575
+ // any non-downscaled frame are 1:1 and unaffected.
576
+ const n = this.toNative(x, y);
577
+ await this.session.desktopInput({ $case: "pointer", pointer: { x: n.x, y: n.y, action, button } });
455
578
  }
456
579
 
457
580
  async screenshot(): Promise<string> {
458
581
  // CRITICAL CONTRACT (mirrors SandboxComputer.screenshot): NEVER return "". The
459
582
  // Agents SDK builds the model image as `data:image/png;base64,${output}`; an
460
- // empty output → `image_url: ''` the model API 400s and kills the turn. A
461
- // missing/empty frame is therefore a THROW, never a silent "". Native capture
462
- // (ScreenCaptureKit / x11) does not have the cold-scrot warm-up the xdotool path
463
- // retries around, so a single capture + a hard empty-guard is sufficient.
464
- const { png } = await this.session.screenshot();
465
- if (png.length === 0) {
466
- throw new ComputerUnavailableError("native desktop screenshot returned an empty frame (display not up?)");
583
+ // empty output → `image_url: ''`. On the hosted computer_use_preview path the SDK
584
+ // catches a throw here, sets output='', and the wire-seam sanitizer rewrites the
585
+ // empty image_url to a BLANK 1×1 placeholder to dodge a 400 — so a capture MISS
586
+ // silently reaches the model as a "blank desktop" it then confidently reports.
587
+ //
588
+ // Native capture (ScreenCaptureKit / x11) can hand back a null/empty FIRST frame
589
+ // in the moments right after the agent connects (the capture stream warming) — the
590
+ // same transient the scrot path retries around for a cold :0. This path previously
591
+ // did a SINGLE capture, so one warm-up miss became a blank. We now RETRY across a
592
+ // bounded budget and, on a terminal failure, FAIL LOUD with the agent's actual
593
+ // reason (logged) — never a silent blank. A permission (TCC) denial is terminal:
594
+ // retrying cannot grant Screen Recording, so we break immediately.
595
+ let lastError: unknown;
596
+ const deadline = Date.now() + this.screenshotWarmupBudgetMs;
597
+ let attempt = 0;
598
+ while (true) {
599
+ if (attempt > 0) {
600
+ await new Promise((r) => setTimeout(r, this.screenshotRetryDelayMs));
601
+ }
602
+ attempt++;
603
+ try {
604
+ const { png, width, height, nativeWidth, nativeHeight } = await this.session.screenshot();
605
+ if (png.length === 0) {
606
+ // A warming capture yields an empty frame — retry rather than hand the model
607
+ // a blank; throw once the budget is spent.
608
+ throw new ComputerUnavailableError("native desktop screenshot returned an empty frame (display not up?)");
609
+ }
610
+ // Record the encoded (what the model sees) vs native geometry of THIS frame so
611
+ // the next click/move/scroll/drag scales its coordinates back to native pixels.
612
+ this.lastEncoded = [width, height];
613
+ this.lastNative = [nativeWidth || width, nativeHeight || height];
614
+ return Buffer.from(png).toString("base64");
615
+ } catch (error) {
616
+ lastError = error;
617
+ // A permission denial will deny on every retry — fail fast with the reason.
618
+ if (isTerminalCaptureDenial(error)) break;
619
+ }
620
+ // Stop once the warm-up budget is spent — the NEXT sleep would push us past it.
621
+ if (Date.now() + this.screenshotRetryDelayMs >= deadline) {
622
+ break;
623
+ }
624
+ }
625
+ // Exhausted the budget (or hit a terminal denial): FAIL LOUD. Log the specific
626
+ // reason so the failure is DIAGNOSABLE (not a silent blank the model misreads),
627
+ // then rethrow — never return "".
628
+ const reason = lastError instanceof Error ? lastError.message : String(lastError);
629
+ console.warn(`[NativeDesktopComputer] screenshot failed after ${attempt} attempt(s): ${reason}`);
630
+ if (lastError instanceof Error) {
631
+ throw lastError;
467
632
  }
468
- return Buffer.from(png).toString("base64");
633
+ throw new ComputerUnavailableError("native desktop screenshot returned an empty frame (display not up?)");
469
634
  }
470
635
 
471
636
  async click(x: number, y: number, button: ComputerButton) {
@@ -482,11 +647,12 @@ export class NativeDesktopComputer implements Computer {
482
647
  }
483
648
  async scroll(x: number, y: number, sx: number, sy: number) {
484
649
  this.guardWrite();
485
- // The model's scroll deltas are PIXELS forward them straight to the agent as a
486
- // ScrollEvent{x,y,deltaX,deltaY} and let the native inject translate to wheel
487
- // events per platform. No xdotool "notch" quantization here (that is an
488
- // xdotool-specific artifact); the agent owns the platform-appropriate scaling.
489
- await this.session.desktopInput({ $case: "scroll", scroll: { x, y, deltaX: sx, deltaY: sy } });
650
+ // The scroll ANCHOR (x,y) is a screenshot-pixel position scale to native like a
651
+ // click. The deltas (sx,sy) are relative scroll AMOUNTS, not positions; the agent
652
+ // owns the platform-appropriate wheel translation, so they pass through unscaled
653
+ // (no xdotool "notch" quantization here that is an xdotool-specific artifact).
654
+ const n = this.toNative(x, y);
655
+ await this.session.desktopInput({ $case: "scroll", scroll: { x: n.x, y: n.y, deltaX: sx, deltaY: sy } });
490
656
  }
491
657
  async type(text: string) {
492
658
  this.guardWrite();
@@ -0,0 +1,25 @@
1
+ // The legible "screen capture failed" error card the history-sanitizer wire seam
2
+ // substitutes for an EMPTY `computer_call_output` image_url (see
3
+ // `rewriteEmptyComputerCallOutputImageUrls`). The hosted `computer_use_preview`
4
+ // protocol has only an IMAGE channel back to the model, so a capture failure has to
5
+ // be a legible IMAGE — otherwise the SDK's empty-output placeholder (a 1×1
6
+ // transparent PNG) reaches the model as a plausible-but-wrong BLANK DESKTOP it then
7
+ // confidently reports (the exact failure mode of the 0.1.3 TCC-denied incident).
8
+ //
9
+ // This is a valid 8-bit RGBA PNG (verified: signature + per-chunk CRC + IDAT inflates
10
+ // to the exact scanline size) rendered from a fixed message, so the provider's image
11
+ // decoder accepts it (an invalid/empty image_url is what 400s the turn). The specific
12
+ // failure REASON (permission denied / null image / timeout / display down) is not in
13
+ // the card — it is logged worker-side by `NativeDesktopComputer.screenshot()`; the
14
+ // card's job is only to stop the model from hallucinating a real blank desktop.
15
+ //
16
+ // Regenerate with: `bun run scripts/gen-screenshot-error-card.mjs`
17
+ // Rendered text:
18
+ // SCREEN CAPTURE FAILED
19
+ // THE DESKTOP COULD NOT BE CAPTURED.
20
+ // THIS IS A PLACEHOLDER, NOT THE REAL SCREEN.
21
+ // DO NOT SAY THE SCREEN IS BLANK.
22
+ // TELL THE USER TO CHECK THE DISPLAY AND GRANT
23
+ // SCREEN RECORDING PERMISSION.
24
+ export const SCREENSHOT_FAILURE_CARD_IMAGE_URL =
25
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABVMAAAGYCAYAAABPvXKBAAAh90lEQVR42u3dMXLkxhJF0VnEGDQQXLY2CaedtkcLkEJBBaqrXmYe43j6bDZQKGTd+RH89df39x8AAAAAAP7bLxcBAAAAAEBMBQAAAAAQUwEAAAAAxFQAAAAAADEVAAAAAEBMBQAAAAAQUwEAAAAAxFQAAAAAAMRUAAAAAAAxFQAAAABATAUAAAAAEFMBAAAAAMRUAAAAAAAxFQAAAABATAUAAAAAQEwFAAAAABBTAQAAAADEVAAAAAAAMRUAAAAAQEwFAAAAABBTAQAAAADEVAAAAAAAMRUAAAAAgMCY+n7dSyV9bpfvdur7Pvnc1Wsy/eGtuNam7RHp63nade7yrE57fk89R9Pev4ZiAAAQU8VUMVVMFWPEVPuVmCqmiqliKgAAiKliqpgqpooxIpCYKqaKqWKqmAoAAGKqmCqmiqliqpgqpoqpnl8xVUwFAAAxVUwVU8VUMVVMFVPFVM+vmCqmAgCAmBp9uKn4uRUPkUmH1y4x1Xq+S34P1/lu848Xnt95P8/9FVgBAEBMFZ/EVDFV5PPzxFTPr5/n/oqpAAAgpopPYqqYaj2LQGKqmGrdmzfEVAAAEFMdbsRUMdV6FoHEVDHVujdviKkAACCm+lwxVUy1nkUgMVVMFVNdZzEVAADEVIcbMfXg53aJO9azCCSmiqnWvXlDTAUAADHV54qpYqr1LAKJqWKqmCqmiqkAACCmOtyIqWKq9Symus5iqtgmpoqpAAAgpjrciBNiqhgjpoqpYqrnV0x1fwEAQEx1yBBTxVQxRkwVU8VUz6+Y6v4CAABbY+qTv8Re8a+zP/luq7/vtM8VU/PX85P7sWNdiam9942k57fidRZTa74HxVQAABBTHap8rpgqpoqpYqqYKqZ674upAAAgpoqpYqqYKqaKqWKqmCqmiqliKgAAiKliqpgqpoqpYqqYKqaKqWKqmAoAAGKqmCpqiqliqpgqpoqpYqqYKqYCAICYWi4qdfkr8/6K8frPrRhopv216Gn3I/06izae34k/z/0VUwEAQEwVUx0ixVQxVaQSUz2/fp77K6YCAICYKqY6RIqpYqqYKqaKqWKq96DnEgAAxFQx1SFSTBVTxVQxVUwVP70HPZcAACCmiqkOkWKqw7qYKqaKqdap6yymAgCAmCqmxv9vHSK/xVQxVUwdsF95fsVU99dzCQAAYqqY6hAppoqpYqqYKqaKqd6DYioAAIipYqpDpEOkmCqmiqliqvjpPSimAgCAmCqmiqkOka6z+yGmiqnWqZjqPQgAAGKqmCqmOkS6zmKqmCra2CfFVO9BAAAQUx8O50+k/y5JUeTU9+18ndMDTef1PC2O7dgnp+1XYqr3kc+t8cwAAICYKqY6vIqpYqqYKqaKqWKqqCmmAgCAmCqmOryKqWKqmCqmiqliqveRmAoAAGKqmCqmiqliqpgqpoqpYqr3kZgKAACIqWKqmCqmiqliqpgqpnof+VxDMQAAhMRUAAAAAAAxFQAAAABATAUAAAAAQEwFAAAAABBTAQAAAADEVAAAAAAAMRUAAAAAQEwFAAAAABBTAQAAAADEVAAAAAAAMRUAAAAAADEVAAAAAEBMBQAAAAAQUwEAAAAAxFQAAAAAADEVAAAAAEBMBQAAAAAQUwEAAAAAEFMBAAAAAMRUAAAAAAAxFQAAAABATAUAAAAAEFMBAAAAAMRUAAAAAAAxFQAAAAAAMRUAAAAAQEwFAAAAABBTAQAAAADEVAAAAAAAMRUAAAAAoGJM/fp9/fmk9+v+uJ9+7urvkfR9Tzn13ZKus/Wcf53T17Nn+tz9PfUcdb6XSWv31M+b9h60N817trz3a66rLvNkxes8bZ4E6E5MFVMdbsRUMdXwK6aKqWKqmGrecJ29F8RUMVVMBUBMFVMN2GKqmCqmOjSLqWKqmGrecJ29F6wrMVVMBUBMFZ8M2GKqmCqmeqbFVOtZTDVvuM6uvZgqpoqpAIiphgWHGzHVehZTPdNiqpgqpop85g3XXkw1T4qpAIyKqRUPQRUPzdPuW1KcSP/cHb+z5/cquR90eY4qHiK9V88Fn2n7Rud/6DHXeS94fl1n718AxFTDgsONmCqmOlQ5NDvMOcyJqQ7h5jrvBe9985X3r5gKIKYaFgzdhiMx1bDv0Oww5zAnprpv5g0x1XvffOX9K6YCiKlijKHbcCSmen7FVPfXdRZTxVT7hrnOe8HP8/61jwOIqV46hm7XWUz1/Iqp9g2HOTFVPDFvmOvcXzHV+9c+DiCmiqmGbte5zSHD0CimOjQ7zFnPa+6HGGPeMNd5L4ip3r/2cQDEVEO34VdM9fyKqQ7N9isxVUw1b5jr7Ffip/ev9y8AYqqh2+HGIcPQKKY6NDvMWc9iqphqrvNe8Py6zt6/AIiphm6HGzHV0CimOjQ7zDnMialiqrnOe0FMdZ29f4UMADHVsGDoFlNdF4cqh2aHOYc5MdUh3LzhveC9b77y/hVTAcTUoJfOajs+1+Em/zrv+Nz09dJ52Pf8OjQ7zImpYkzm/U16/3ovXOb2D1wr81XN59c+DiCmiqliqqFMTDXse37FVDFVTBVTxVTvBTFVTBVT7eMAiKliqpgqphr2Pb9iqpgqpoqpYqqYKqaKqWKqmAqAmGooE1PFVDFVTBVTxVQxVUwVU8VUMVVMFVPFVADEVEOZmCqmiqliqpgqpjrMialiqpgqpoqp5isxFQAx1V+r9Nd1h1zn9EO959fza98QU7vF1NX/YCWmmje8F9xf89WMf5yyjwMgphq6Db9iqufX8+vQLKaKqWKqecN7wXtBTBVTvX8BEFMNZQ43Yqqh0fPrkCGmiqliqpjqveC9IKaKqd6/AIiphjJDnpgq2lhXDs1iqsOcmCqmei94L4ipYqr1DICYKsY43Iipoo115dAspjrMiakO4d4L3gtiqphqPYupAGKqYcHQ7Tpv/77+2qzn1yFDTBVT+60rMdV7wXtBTJ1wndNn4PT79uQfCwEQUw1lhl8xVUz1/Do0i6liqpgqpnoveC/Y18RUMVVMBRBTDWUON2KqmGpdOTSLqWKqmOp96b3gvSCmiqnev2IqgJhqKHO4cTgUU60rh2YxVZSzrsRU7wXvBTFVTBVTxVQAMdWwYOgWU8VU68qhWUwVU60rMdV7wXvB/TVfialiKoCYalj4kPQhb8d3O3Wdk77vtMN6xevc5SCd/kxXXFed9ytzxBV1nTvH1M7P77RrL6aarzqdy5L2CDEVQEwVU8VUMVVMFVM902KqmCqmiqliqpgqpoqpYqqYCiCmiqliqjghpoqpnmkxVUwVU8VU84aYKqaar8RUMRVATBVTxVRxQkwVUwULMVVMFVPFVPOGmCqmmq/EVDEVQEwVU8VUMVVMNeyLqWKq/cocIabam8RUMVVMdS4TUwHEVAAAAAAAxFQAAAAAADEVAAAAAEBMBQAAAAAQUwEAAAAAxFQAAAAAADEVAAAAAEBMBQAAAAAQUwEAAAAAEFMBAAAAAMRUAAAAAAAxFQAAAABATAUAAAAAEFMBAAAAAMRUAAAAAAAxFQAAAAAAMRUAAAAAQEwFAAAAABBTAQAAAADEVAAAAAAAMRUAAAAAQEwFAAAAABBTAQAAAADEVBcBAAAAAEBMBQAAAAAQUwEAAAAAxFQAAAAAADEVAAAAAEBMBQAAAAAQUwEAAAAAxNQPeL/uj/vp567+Hknfd8fvkrRwXeea177zd1u9hqbd31Pvhc7vo1P399R67vIOOLWeDcWX5zdsvuo8TzqnXN4LBX6XinOstdZn3ugyX1U8LyCmiqmGI9dZTBVTxVTDkZhq2BdTxVQx1XvB/RVTxVRrTUx1XkBMNaSIqa6zmCqmiqmGI4cqw76YKqaKqc4pYqr3gphqrYmpzgueSzHVkCLyialiqpgqphqOxFSHZjFVTBVTnVPEVO8F731nCDFVTBVTxVRDisgnpoqpBiExVUwVUx1kxFQxVUwVU8VU7wUxVUwVU8VUMVVMbTGs+nkOQa7zzOuXdLjxffO/77TrbD37vuaIfdHB3Gke6rRfmeu8B60rn+u8kDlvIKb6eR4W19n1Myz4voZQ99f39X4TU60X5xTRy3vBujJPWldiqrlOTPXzPCyus+tnWPB9DaFiqvXs/SamWi/mIdHLdfZ9xVTrSkz1nhZTxU9DrcOm62foNhwZQsVU19n+LKaaO81Dopf3gu8rplpXrrOYKqaKlYZa68r1E1MN3YYjMdV1tj+LqWKqeUj0Mtf5vtaVOdZ5QUwVUw0phlpDlGFBTDUsGI7EVNdZTB3/Hjz131kv5iHRy3W2rsRU62rOdfZciqnip6HWunL9xFTDkeFITHWd7c9iqrnTPCR6eS/4vmKqOdZ1FlPFVLHSUGtduX5iqqHbcCSmus72ZzFVTDUP2a/Mdd6D1pXPdV4QU8VUQ4qh1hBlWBBTDQu+r5hqPYup5ggx1Txkv3LfvAetK5/rORJTxVRDip/nZec6i6mGBd/XdXZ/fV9zhJhqvYip7pv3gnVljvUciamMj6mrpT+kp77btJdd5+uc/hylf98ufzVy9TXo8n2nXWeHyLv1ujIUi6liav9zyrR9Y9r+PG2u67LWpp0XfK6YKqYaUkQ+MVVMFVMNR2KqmCqmiqkimpgqpoqpYqp5Q0x1XvAcialiqiFFTDX8us5iqpgqpjrciKliqpgqpjqkianmWDFVtBFTPUdiKmKqmCqmus5iqpgqpjrciKliqgFbTBVTxVRzrAgkpoqpniMxFTFVTBVTXWcxVUw1dDvciKliqpgqpoqpziliqggkpoqpniMx1awnpvp5zf9K4bTv4a/bux/ik+tc5TqLqTW/b/rhq/McUfEfQ8TU3ucK9+1u/XxYV9ap52jmecE/nIupYqqoJFaKqWKqodt1FlOtZzFVTDXHmodELxHIuhJTxVTnBTFVTBU/RSXfw+/nfhi6XWcx1XUWU8VUMVVMFb1EIO9BzBvOZWKqmGpIEZXEVL+f72voFlPFVNdZTBVTxVQx1X4lpnoPCi3mDc+RmCqmGlJEJTHV72fd+1yxTUwVUx1uxFQxVZQTU0Uq84aYKqZ6jsRUxFRDqJjq97PuvbTFJ9fZ/R30fXf8zuaI3odSUU5M7X7fuvzVdetKTPUcOS+Is2Kqnyfyuc6eSy9PQ7frLKb6vmKqmOp9bh4SvUQg60pM9Rw5L4ipYqqYKiqJlWKqmGrodp3FVOtZTPUeNMdaB6KXCGRdialiqvOCmCqmip+iku/h93M/vLRdZzHVdRZTRTQx1ToQvcRU+xXmDecyMVVMNaSISmKq38/39bliqhjjOoup5ggxVZSzX4mp3oMii3nDcySmiqmGlAWb3Go7PrfiS8d1FlM7vTxPref0l3bS8zvtOtufPb8Tw9q059c9d07pNNs6D1pXFeb29N/Fc2SuE1PFVA+9mOo6i6liqpjqOoupYqqYKqaKqc4pYqqYal2JqdaVuU5MFVM99CKf6yymiqliquFITBVTxVQxVUx1ThG9nAetKzHVunJeEFPFVC9Pkc919lyKqV7ahiMxVUwVU8VUMdU8JHq5zr6vmCqmOi+IqWKqIUXkE1MdHsRUL23DkZgqpoqp3oNiqnUgeompopeYKqaKqWKqmAoAAAAAIKYCAAAAAIipAAAAAACIqQAAAAAAYioAAAAAgJgKAAAAACCmAgAAAACIqQAAAAAAYioAAAAAgJgKAAAAACCmuhAAAAAAAGIqAAAAAICYCgAAAAAgpgIAAAAAiKkAAAAAAGIqAAAAAICYCgAAAAAgpgIAAAAAIKYCAAAAAIipAAAAAABiKgAAAACAmAoAAAAAIKYCAAAAAIipAAAAAABiKgAAAAAAYioAAAAAgJgKAAAAACCmAgAAAACIqQAAAAAAYioAAAAAgJj6L96ve6lTv0v6jdxxrZKu8+p11eWep1/7iuu54r386fc9dZ09vzWf31P3t+Jz1Pn5nbZPps+2nfemitcgfc6Ztm8AgJgqpoqpYoyYKhKIqZ5fMVVMtU+KqWKqmCqmAoCYKqaKqWKqmCoSiKmeXzFVTBVFxFR7k5gqpgKAmCqmiqliqhgjpoqpnl8xVUwVU8VUMVVMFVMBQEwVU8VUMdWBR0wVU8VUMVVMFVPFVDFVTBVTHbABEFMLRz6HjD1DXvp17jLknfoenl//aFLh/np+7Ruuc6+YZY6Yd/3sV/lzu3AKAGKqmCqmijGeX4dcMdXza98QU+2TApLnV0z1LACAmCqmiqkOh55f68Ch2fNr3xBTxVTvD8+vmCqmAoCYKsaIqSKaKGIdiKmeX/uG6yymen94fsVUMRUAxFQxRkx1CHLIsA7EVHHC54qpYqo5QkwVU8VUABBTxZiA63zqv3MIEkV2DfGdh30x1X2zb4ipVd7n9kn7lZiaN497TwOAmCqmiqliqpgqptqfPb/2DddZPDFHeH7FVPsBAIipYqqY6hAkpjoAiKnum88VU8VUc4SYKqaKqQAgpoqpYqpDkCgipoqp4oTPFWPEVDHV9bNfiakAIKaKqWKqQ5AoIqaKqQ5pIp8YI6baJ10/MVVMBQAxVUwVU8XU4Hu+46/Drub+Zj0fO+5v0hqa9vymf7ek/ari54one75H5/0qfT2nf9+KkVQ4BQAxVUwVU8VUMVVMFVM9v2KqmCqmiqliqpgqpgKAmCqmiqlim5gqpoqpYqqY6nPFVDFVTBVTxVQAEFPFVDFVTBVT3V8xVUwVJ8RUMVVMFVPFVDEVAMRUMVVMFVPFVDFVTBVTxVQxVUwVU8VUMVVMBQAxVUwtfnjYMQyKqVfr+O7++ivV/kpw/+fXe7rP5077hwq/n/dCp++bFHEBQEw1pIipYqoY4/6KqWKq59f+IqaKqX4/MVVM9Z4GADFVTBVTDYNiqpgqpnp+7S9iqpjq9/NeEFO9pwFATBVtxFSxTUwVUx2aPb9iqpgqpoqp3gtiqpgKAGKqmCqmim1ih/srpiKmiqliqlhpbvd9xVQAEFMd0lxnsU1MdX/FVM+lOHH0fyviXmKq309MHTA/J+2xnf9RBwAx1ZBi+BXbxBj3V0wVUz2/YqqYKqb6/exXYqqYCgBiquFXbBNj3F8xVUz1/IqpYqqY6vcTU8VUMRUAxFQx1XU2lLm/YqpDs+dXTBVTxVS/n/eC+VlMBQAxVUx1ncU291dMFVM9v2KqmGo/FVPNk+ZnMRUA2sfUJ079LiLLXfI6dz4c7rjOYqrnfNdhLuld4T14b/lr0V3WVdLnTot8Fe+veePyHPk/QYipAIipYqrIIqaKqWKqmCqmiqliqpgqpoqpYqrPFVMBEFMdIsVUMdXhRkz1nIup3oNiqpgqpoqpYqqYKqYCgJgqpoqpYqqYKqaKqd6DYqqYKqaKqWKqmCqmAoCYKqaKqWKqmCqmiqliqpgqpoqpYqqYKmqKqQBQKaYCAAAAAIipAAAAAABiKgAAAAAAYioAAAAAgJgKAAAAACCmAgAAAACIqQAAAAAAYioAAAAAgJgKAAAAACCmAgAAAACIqQAAAAAAiKkAAAAAAGIqAAAAAICYCgAAAAAgpgIAAAAAiKkAAAAAAGIqAAAAAICYCgAAAACAmAoAAAAAIKYCAAAAAIipAAAAAABiKgAAAACAmAoAAAAAIKYCAAAAAIipAAAAAACIqQAAAAAAYioAAAAAgJgKAAAAACCmAgAAAACIqQAAAAAAYuo/vV/3x6V/7o4beepzT+lyf9PXc9L9PfXzpu1XXdZQ+nPU+TonPUfT9ufO19m8Yd6oujfZN7Kuc8W1VnF+dp3z92fzZP51tp6z17iYKqbaSB1uxFQxVUwVU8VUMVVMNW+YN8RUMVXkE1PFVPOkmGreEFPFVDHV4UZMFVMNZWKqmCqmiqnmDfOGmCqmiqliqvhknhRTzRtiqpgqpjrciKliqpgqphp+7c9iqnnDvCGmiqliqpgqPpknxVQxVUwVU8VUhxuHGzFVTBVTDb/2ZzHVvGHeEFPFVDFVTBWfxFQx1XoeFVOnbf5i6iXKbdps3Lc+17nL53Ye1Fxn69m+8Zl/nPLesp7dN+cj+3Pm9/V/vngWgdxf82SF++sfdcRUhxsx1eHGfTN0i3z2DcOvfUNMtW+YN+z39mff17whprq/nl8xVUwVNcVUhxuHG0O3fde+Yfi1b4ip9g3zhvtmfxZTxTYx1f31/IqpYqphQUw1JDvcGLrFVOvP8GvfEFO9t8wb7pvzkf1ZbBNT3V/Pr5gqphoWxFTryuHGdTYsWH+GX/uG6yymWs/um/OR/VlMFVPdX/uV9SymOtT7XEOyw42XmH1DTHWdDb/2De8t84b75v1hPQfet87Pfvq6OvXfmSftV9apmOrhE1MNyQ43hm77rn3D8GvfEFPtG+YN9835SEwVU8VU86T9SkwVUw0LYqp15XDjOhv2rT/Dr31DTPXesp7dN+cj+7OYKqaKVPYrMVVMNSyIqdaVw43rbFgQUw2/hl8x1XvLenbfnI/sz2KqmCpS2a/EVDHVod7nGpIdbrzE7BtiqutsPds3vLfMG+6b94f1LKY6L4hU5kkxVUxtvRk+IaYakj+xrlb/lb3V69nh5jq2b3T+XOvP8Lvrczvvz+nX2bxh3jBv1Njvp805nd9H/vH73D4pUmXtz13mus7rWUwVU8VUQ7LDjZgqpoqpYqqYKhKIqeYN84aY6lwmpoqpIpWYKqaKqYZQMdXhxuHG4UZMFVPFVDFVTBVTzRvmDTFVTBVTxVQxVUwVU61TMVVMdbhxuHG4ccgQUx2uxVQxVUw1b5g3xFQxVUwVU8VUMVVMtU7FVC9tMdXhxuFGTBVTHa7FVDFVTDVvmDfs9+YcMVVMFVPFVDHVOhVTRU0x1V/X9dd1/dXr3/4qt31DTPXXou0b9g33zX3Lv372SevKe/8zkc/+kv/X7a1nMVU88blesg437puYav05BDm8mjfsG+YN981+b5+0rsQ2MVVMFVPFVMOMl7Yh2eHGfXPIsP4cgrwHRQL7hnnDfbPf2yetK8+vmOr+iqliqpjqpS2mOty4b6KD9ecQJKaKBPYN+4b7Zr8351hXYqqYKqaKqWKqmCpqiqkON+6b6GD92TfEVDHVvmHfcN/s92KqmCqmiqnmSTFVTBVTvTzFVMOMw42YWuavZFp/DkFiqnnDvmHecN/s9/ZJ68p7v8/nur93yRlpWhQWU8VUMdUw4/6KqaKI9Wf4tT+LqfYN84b7JqZ6H4mp4pP9RUwVU8VU8URMta4cbuwHooj1Z/g1dIup9g3r2X0TU+2T1pX3vpgqplpXYqp4IqZ6yTrc2A/EVOvP8CummjfsG+YN981+b5+0rrz3xVQxVUwVU710fK4h2eHGfXPIEEUMFQ6v3vv2DfOGecN+b5+0rsQ20cv9FVPF1EEPy2o+10v25CY37f7u+G6nrmnSvmE43/M97MV91nPS/mzfMG+YN/L2ps7nhYpR0z5p3+0UU82TvedJMVVMFVPFVIcbhxsx1bAvpoqpYqqYat8wb4ipYqqYKk6IqWKqeVJMFVPFVDHVS9bhRkwVUw3nYqrhV0y1b5g3xFQxVUwVJ8RUMdU8KaaKqWKqmOol63AjpoqphnMx1fArpto3zBtiqpgqpooT9l0x1TwpptqvxFQxVUx1uBFTHaoM52Kq4VdMtW+YN8RUMVVMFVPtu2KqedK6sl+Vi6kAAAAAAMnEVAAAAAAAMRUAAAAAQEwFAAAAABBTAQAAAADEVAAAAAAAMRUAAAAAQEwFAAAAABBTAQAAAAAQUwEAAAAAxFQAAAAAADEVAAAAAEBMBQAAAAAQUwEAAAAAxFQAAAAAADEVAAAAAAAxFQAAAABATAUAAAAAEFMBAAAAAMRUAAAAAAAxFQAAAABATAUAAAAAEFMBAAAAAMRUFwIAAAAAQEwFAAAAABBTAQAAAADEVAAAAAAAMRUAAAAAQEwFAAAAABBTAQAAAADE1B3er3uppM/t8t1Ofd8nn7t6TSY9qNPWVZdrYE+87Bth195+1fu9v2M9ey9creerpPdg57kOABBTxVSHSDHVM+PQLAiIqWKqdSWm2sPEVDEVABBTHbTEVDFVnHBo9t3EVDHVuhJTvRfEVDEVAEBMFVPFVDFVTBVTxVQx1X4lpoqpYqqYCgAgpoqpYqqYKqaKqdaVmGq/ElPFVDFVTHXwAwDKxNRTw0yXzz0VCSpe56Qh3rry/J68zl3+UWLHz9uxb0w71NuvsqLmjmehcxwzH+SvZ/suACCm+lzRS0y1rjy/YqqYKqaKqWKqmCqmiqkAgJgqxjhEiqnWledXTBVTxVT7lZhqnhRTxVQAQEw1/DpEiqnWledXTBVTxVT7lZgqpoqpYioAIKYafh0ixVRxwvMrpoqpYqr9SkwVUz1vYioAIKaKIg6RTa9zl9hhXXl+xdS86yKm2q+sUzFVTLXvAgBiqijiECmmOixZV2KqQ71Dvf3KOhVTzQfWMwCAmOpQJaaKqdaVmCqmOtR7jsRUMVVMtZ4BAMRUhyrXWUy1rlxn8cSh3n5l37AfiKnWs5gKAIipYozoJaZaV66zmOpQL6aKqWKqmGrfte8CAK1j6teDv9hZ8a94Pvluq7/vtM+dFlPd33l/lTv9OjvUr99fdvzVdfuV92DV+LnjOid9rn1XTAUAxFQx1SFSTHV/xVQx1aFeTPUeFFPFVPuumAoAiKliqkOkmGpdialiqkO9mOo9KKaKqWKqmAoAiKliqsOcQ6Q4IaaKqQ71Yqr9yntQTBVTxVQAQEwVUx0iHSLFCTFVTHWoF1PtV96DYqqYKqYCAETE1C9/1ddfuT34uV3ihHXlr3KLnzX2jS7PpX3j8vxap+Jn4PNRca4DAMRUMVX0ElOtK4dcMVVMFVPtG55f69R9E1MBADFVTBW9xFQx1boSU8VUMdW+IaZap2KqmAoAIKY6VImpYqp1Jab6eWKq51dMtU7FVDEVAEBMdahyGBFTrSsx1aFeTBWp7M9iqpgqpjr4AQBiavGYmvS/dRjpfeizruYdIh3q+9xfkWreP+553qxT+2TdfzwDAMRUMVX0chgRU60rMdXPE1PFVDHVOhVTxVQxFQAQUx2qHEbEVOtKTHWoF1M9v/Zn69T8Yp8UUwEAMdWhyuHVoc+6ElMd6kUCkcr+7L3qPWOfFFMBADFVTHV4deizrsRUMUYkEKm898VU7xn7pJgKALSLqT8dSHYMLjt+l6Qh9NT37Xyduxz60u9v0l5y6jMq3t9T97LzYb3ioT4pUk1775s3vHu8B8VUAEBMFVPFVDFVTBVTxVQxVUy1rswb3j3eg2IqACCmiqkON2KqmCqmiqliqpgqpoqpYqqYKqYCAGKqmOpwI6aKqWKqmGrfEFPFVDFVTBVTxVQAQEwVUx1uRBEx1YFWTBVTxVQxVUwVU8VUMRUAEFMBAAAAABBTAQAAAADEVAAAAAAAMRUAAAAAQEwFAAAAABBTAQAAAADEVAAAAAAAMRUAAAAAADEVAAAAAEBMBQAAAAAQUwEAAAAAxFQAAAAAADEVAAAAAEBMBQAAAAAQUwEAAAAAxFQAAAAAAMRUAAAAAAAxFQAAAABATAUAAAAAEFMBAAAAAMRUAAAAAAAxFQAAAABATAUAAAAAQEwFAAAAAPh//gZFPTu5uGIZdgAAAABJRU5ErkJggg==";