|
| 1 | +import fs from "node:fs" |
| 2 | +import os from "node:os" |
| 3 | +import path from "node:path" |
| 4 | + |
| 5 | +const argv = process.argv.slice(2) |
| 6 | + |
| 7 | +function takeValue(name) { |
| 8 | + const index = argv.indexOf(name) |
| 9 | + if (index === -1) return null |
| 10 | + if (!argv[index + 1]) { |
| 11 | + console.error(`Missing value for ${name}.`) |
| 12 | + process.exit(2) |
| 13 | + } |
| 14 | + const value = argv[index + 1] |
| 15 | + argv.splice(index, 2) |
| 16 | + return value |
| 17 | +} |
| 18 | + |
| 19 | +function takeFlag(name) { |
| 20 | + const index = argv.indexOf(name) |
| 21 | + if (index === -1) return false |
| 22 | + argv.splice(index, 1) |
| 23 | + return true |
| 24 | +} |
| 25 | + |
| 26 | +const jsonOutput = takeFlag("--json") |
| 27 | +const requestedRoot = takeValue("--sessions-root") |
| 28 | +if (argv.length > 0) { |
| 29 | + console.error(`Unknown arguments: ${argv.join(" ")}`) |
| 30 | + process.exit(2) |
| 31 | +} |
| 32 | + |
| 33 | +function boundedInteger(value, fallback, minimum, maximum) { |
| 34 | + const parsed = Number.parseInt(value ?? "", 10) |
| 35 | + if (!Number.isFinite(parsed)) return fallback |
| 36 | + return Math.min(Math.max(parsed, minimum), maximum) |
| 37 | +} |
| 38 | + |
| 39 | +const MiB = 1024 * 1024 |
| 40 | +const warningBytes = boundedInteger(process.env.OPERATOR_CODEX_IMAGE_ROLLOUT_WARNING_MIB, 64, 1, 4096) * MiB |
| 41 | +const criticalBytes = boundedInteger(process.env.OPERATOR_CODEX_IMAGE_ROLLOUT_CRITICAL_MIB, 256, 2, 16384) * MiB |
| 42 | +const maxFiles = boundedInteger(process.env.OPERATOR_CODEX_IMAGE_ROLLOUT_MAX_FILES, 20000, 1, 200000) |
| 43 | +const maxCandidateScans = boundedInteger(process.env.OPERATOR_CODEX_IMAGE_ROLLOUT_MAX_CANDIDATE_SCANS, 100, 1, 1000) |
| 44 | +const maxTotalScanBytes = boundedInteger(process.env.OPERATOR_CODEX_IMAGE_ROLLOUT_MAX_SCAN_MIB, 1024, 1, 16384) * MiB |
| 45 | + |
| 46 | +if (criticalBytes <= warningBytes) { |
| 47 | + console.error("Critical rollout threshold must be greater than the warning threshold.") |
| 48 | + process.exit(2) |
| 49 | +} |
| 50 | + |
| 51 | +const codexHome = path.resolve(process.env.CODEX_HOME || path.join(os.homedir(), ".codex")) |
| 52 | +const sessionsRoot = path.resolve(requestedRoot || path.join(codexHome, "sessions")) |
| 53 | + |
| 54 | +function isInside(root, candidate) { |
| 55 | + const relative = path.relative(root, candidate) |
| 56 | + return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)) |
| 57 | +} |
| 58 | + |
| 59 | +async function containsInlineImage(filePath, byteBudget) { |
| 60 | + const marker = Buffer.from("data:image/") |
| 61 | + const stream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 }) |
| 62 | + let tail = Buffer.alloc(0) |
| 63 | + let scanned = 0 |
| 64 | + |
| 65 | + try { |
| 66 | + for await (const chunk of stream) { |
| 67 | + const remaining = byteBudget - scanned |
| 68 | + if (remaining <= 0) return { found: false, scanned, complete: false } |
| 69 | + const bounded = chunk.length > remaining ? chunk.subarray(0, remaining) : chunk |
| 70 | + const combined = tail.length > 0 ? Buffer.concat([tail, bounded]) : bounded |
| 71 | + scanned += bounded.length |
| 72 | + if (combined.indexOf(marker) !== -1) return { found: true, scanned, complete: true } |
| 73 | + tail = combined.subarray(Math.max(0, combined.length - marker.length + 1)) |
| 74 | + if (bounded.length < chunk.length) return { found: false, scanned, complete: false } |
| 75 | + } |
| 76 | + return { found: false, scanned, complete: true } |
| 77 | + } finally { |
| 78 | + stream.destroy() |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +const findings = [] |
| 83 | +let filesVisited = 0 |
| 84 | +let candidateScans = 0 |
| 85 | +let scannedBytes = 0 |
| 86 | +let traversalTruncated = false |
| 87 | +let scanBudgetExhausted = false |
| 88 | + |
| 89 | +async function walk(directory) { |
| 90 | + if (filesVisited >= maxFiles) { |
| 91 | + traversalTruncated = true |
| 92 | + return |
| 93 | + } |
| 94 | + |
| 95 | + let entries |
| 96 | + try { |
| 97 | + entries = await fs.promises.readdir(directory, { withFileTypes: true }) |
| 98 | + } catch (error) { |
| 99 | + findings.push({ |
| 100 | + severity: "critical", |
| 101 | + code: "sessions_directory_unreadable", |
| 102 | + path: directory, |
| 103 | + message: String(error?.message || error), |
| 104 | + }) |
| 105 | + return |
| 106 | + } |
| 107 | + |
| 108 | + for (const entry of entries) { |
| 109 | + if (filesVisited >= maxFiles) { |
| 110 | + traversalTruncated = true |
| 111 | + return |
| 112 | + } |
| 113 | + |
| 114 | + const candidate = path.resolve(directory, entry.name) |
| 115 | + if (!isInside(sessionsRoot, candidate)) { |
| 116 | + findings.push({ severity: "critical", code: "path_escape", path: candidate }) |
| 117 | + continue |
| 118 | + } |
| 119 | + |
| 120 | + let stat |
| 121 | + try { |
| 122 | + stat = await fs.promises.lstat(candidate) |
| 123 | + } catch (error) { |
| 124 | + findings.push({ severity: "critical", code: "state_entry_unreadable", path: candidate, message: String(error?.message || error) }) |
| 125 | + continue |
| 126 | + } |
| 127 | + |
| 128 | + if (stat.isSymbolicLink()) { |
| 129 | + findings.push({ severity: "critical", code: "symlink_in_sessions", path: candidate }) |
| 130 | + continue |
| 131 | + } |
| 132 | + if (stat.isDirectory()) { |
| 133 | + await walk(candidate) |
| 134 | + continue |
| 135 | + } |
| 136 | + if (!stat.isFile() || !entry.name.endsWith(".jsonl")) continue |
| 137 | + |
| 138 | + filesVisited += 1 |
| 139 | + if (stat.size < warningBytes) continue |
| 140 | + |
| 141 | + let imageScan = { found: false, scanned: 0, complete: false } |
| 142 | + if (candidateScans < maxCandidateScans && scannedBytes < maxTotalScanBytes) { |
| 143 | + const perFileBudget = Math.min(stat.size, maxTotalScanBytes - scannedBytes) |
| 144 | + candidateScans += 1 |
| 145 | + imageScan = await containsInlineImage(candidate, perFileBudget) |
| 146 | + scannedBytes += imageScan.scanned |
| 147 | + if (!imageScan.complete && !imageScan.found) scanBudgetExhausted = true |
| 148 | + } else { |
| 149 | + scanBudgetExhausted = true |
| 150 | + } |
| 151 | + |
| 152 | + const severity = stat.size >= criticalBytes || imageScan.found || !imageScan.complete ? "critical" : "warning" |
| 153 | + const code = stat.size >= criticalBytes |
| 154 | + ? imageScan.found |
| 155 | + ? "oversized_inline_image_rollout" |
| 156 | + : "oversized_rollout" |
| 157 | + : imageScan.found |
| 158 | + ? "inline_image_rollout" |
| 159 | + : imageScan.complete |
| 160 | + ? "large_rollout" |
| 161 | + : "large_rollout_scan_incomplete" |
| 162 | + |
| 163 | + findings.push({ |
| 164 | + severity, |
| 165 | + code, |
| 166 | + path: candidate, |
| 167 | + bytes: stat.size, |
| 168 | + inline_image_marker_found: imageScan.found, |
| 169 | + bytes_scanned: imageScan.scanned, |
| 170 | + scan_complete: imageScan.complete, |
| 171 | + }) |
| 172 | + } |
| 173 | +} |
| 174 | + |
| 175 | +let rootStatus = "present" |
| 176 | +try { |
| 177 | + const rootStat = await fs.promises.lstat(sessionsRoot) |
| 178 | + if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) { |
| 179 | + findings.push({ severity: "critical", code: "invalid_sessions_root", path: sessionsRoot }) |
| 180 | + } else { |
| 181 | + await walk(sessionsRoot) |
| 182 | + } |
| 183 | +} catch (error) { |
| 184 | + if (error?.code === "ENOENT") rootStatus = "not_present" |
| 185 | + else findings.push({ severity: "critical", code: "sessions_root_unreadable", path: sessionsRoot, message: String(error?.message || error) }) |
| 186 | +} |
| 187 | + |
| 188 | +if (traversalTruncated) findings.push({ severity: "critical", code: "file_traversal_limit_reached", limit: maxFiles }) |
| 189 | +if (scanBudgetExhausted) findings.push({ severity: "critical", code: "scan_budget_exhausted", max_total_scan_bytes: maxTotalScanBytes }) |
| 190 | + |
| 191 | +const criticalCount = findings.filter((item) => item.severity === "critical").length |
| 192 | +const warningCount = findings.filter((item) => item.severity === "warning").length |
| 193 | +const desktopSafeToLaunch = criticalCount === 0 |
| 194 | + |
| 195 | +const result = { |
| 196 | + schema_version: 1, |
| 197 | + status: desktopSafeToLaunch ? (warningCount > 0 ? "warning" : "clean") : "blocked", |
| 198 | + sessions_root: sessionsRoot, |
| 199 | + sessions_root_status: rootStatus, |
| 200 | + warning_threshold_bytes: warningBytes, |
| 201 | + critical_threshold_bytes: criticalBytes, |
| 202 | + files_visited: filesVisited, |
| 203 | + candidate_files_scanned: candidateScans, |
| 204 | + bytes_scanned: scannedBytes, |
| 205 | + desktop_safe_to_launch: desktopSafeToLaunch, |
| 206 | + findings, |
| 207 | + continuity_route: desktopSafeToLaunch |
| 208 | + ? "guarded_direct_openai" |
| 209 | + : "fresh_isolated_codex_home_or_explicitly_authorized_local_route", |
| 210 | + mutation_performed: false, |
| 211 | +} |
| 212 | + |
| 213 | +if (jsonOutput) console.log(JSON.stringify(result, null, 2)) |
| 214 | +else { |
| 215 | + console.log(`Codex image-rollout preflight: ${result.status}`) |
| 216 | + console.log(`Sessions root: ${sessionsRoot}`) |
| 217 | + console.log(`Files visited: ${filesVisited}; critical: ${criticalCount}; warnings: ${warningCount}`) |
| 218 | + for (const finding of findings) { |
| 219 | + console.log(`${finding.severity.toUpperCase()} ${finding.code}${finding.path ? ` ${finding.path}` : ""}`) |
| 220 | + } |
| 221 | + if (!desktopSafeToLaunch) { |
| 222 | + console.error("Refusing Desktop admission for this state profile. Do not delete or rewrite rollout history. Preserve the profile, continue through a fresh isolated CODEX_HOME or explicitly authorized local route, and reconcile uncertain writes before replay.") |
| 223 | + } |
| 224 | +} |
| 225 | + |
| 226 | +process.exit(desktopSafeToLaunch ? 0 : 75) |
0 commit comments