fix(macos): prevent mismatched CUA driver endpoints (#123845)

* fix(macos): make CUA driver endpoint handoff atomic

* ci: record approved CUA env budget increase

* test: satisfy changed-gate hygiene

* style(macos): use failable endpoint string conversion

* fix(macos): strip retired CUA worker environment

* ci: constrain CUA env budget approval

* test(macos): isolate inherited CUA environment
This commit is contained in:
Peter Steinberger
2026-08-14 16:04:43 -07:00
committed by GitHub
parent f726393812
commit 98a8e0d63f
13 changed files with 197 additions and 83 deletions
@@ -69,14 +69,19 @@ enum ComputerControlProvider: String, CaseIterable, Sendable {
}
}
struct CuaDriverWorkerEndpoint: Equatable, Sendable {
struct CuaDriverWorkerEndpoint: Encodable, Equatable, Sendable {
private let v = 1
let socketPath: String
let binaryPath: String
func environmentValue() throws -> String {
try String(bytes: JSONEncoder().encode(self), encoding: .utf8)!
}
}
enum CuaDriverWorkerEnvironment {
static let socketPath = "CUA_DRIVER_SOCKET_PATH"
static let binaryPath = "CUA_DRIVER_BINARY_PATH"
static let endpoint = "OPENCLAW_CUA_DRIVER_ENDPOINT"
static let inheritedFamilyPrefixes = [String(endpoint.dropLast("ENDPOINT".count)), "CUA_DRIVER_"]
}
enum CuaDriverArtifact {
+3 -3
View File
@@ -406,11 +406,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var profileInstanceLock: AppInstanceLock?
private let webChatAutoLogger = Logger(subsystem: "ai.openclaw", category: "Chat")
var nodeTerminationCleanup: @MainActor () async -> Void = {
// CUA shutdown drains the worker before closing the daemon socket; run it
// first so other cleanup cannot consume the app termination deadline.
await CuaDriverHostCoordinator.shared.shutdown()
await TalkMLXSpeechSynthesizer.shared.shutdown()
await MacNodeModeCoordinator.shared.stopAndWait()
// The worker owns the MCP proxy. Stop it before closing the app-owned
// daemon socket so an in-flight completion cannot be mistaken for retryable.
await CuaDriverHostCoordinator.shared.shutdown()
}
var peekabooBridgeTerminationCleanup: @MainActor () async -> Void = {
@@ -342,9 +342,9 @@ final class MacNodeHostWorker: MacNodeHostWorking, @unchecked Sendable {
self.finishStartLocked(.failure(WorkerError.unavailable("could not protect worker input pipe")))
return
}
var environment = ProcessInfo.processInfo.environment
environment.removeValue(forKey: CuaDriverWorkerEnvironment.socketPath)
environment.removeValue(forKey: CuaDriverWorkerEnvironment.binaryPath)
var environment = ProcessInfo.processInfo.environment.filter { key, _ in
!CuaDriverWorkerEnvironment.inheritedFamilyPrefixes.contains { key.hasPrefix($0) }
}
environment.merge(launch.environment, uniquingKeysWith: { _, explicit in explicit })
environment["PATH"] = CommandResolver.preferredPaths().joined(separator: ":")
environment["OPENCLAW_NODE_EXEC_HOST"] = "app"
@@ -946,8 +946,7 @@ extension MacNodeModeCoordinator {
}
var workerEnvironment: [String: String] = [:]
if provider == .cua, let endpoint = CuaDriverHostCoordinator.shared.workerEndpoint {
workerEnvironment[CuaDriverWorkerEnvironment.socketPath] = endpoint.socketPath
workerEnvironment[CuaDriverWorkerEnvironment.binaryPath] = endpoint.binaryPath
workerEnvironment[CuaDriverWorkerEnvironment.endpoint] = try endpoint.environmentValue()
}
let effectiveLaunch = MacNodeHostWorkerLaunch(
command: launch.command,
@@ -47,4 +47,19 @@ struct ComputerControlSettingsTests {
try FileManager.default.createSymbolicLink(at: binary, withDestinationURL: target)
#expect(CuaDriverArtifact.executableURL(in: root) == nil)
}
@Test func `CUA worker endpoint uses versioned JSON with escaped paths`() throws {
let endpoint = CuaDriverWorkerEndpoint(
socketPath: #"/tmp/openclaw-"quoted"/cua.sock"#,
binaryPath: #"/Applications/OpenClaw\Test.app/Contents/Resources/cua-driver"#)
let value = try endpoint.environmentValue()
let decoded = try #require(
JSONSerialization.jsonObject(with: Data(value.utf8)) as? [String: Any])
#expect(decoded.count == 3)
#expect(decoded["v"] as? Int == 1)
#expect(decoded["socketPath"] as? String == endpoint.socketPath)
#expect(decoded["binaryPath"] as? String == endpoint.binaryPath)
}
}
@@ -418,11 +418,13 @@ struct CuaDriverHostCoordinatorTests {
await coordinator.setEnabled(true)
let originalEndpoint = try #require(coordinator.workerEndpoint)
let originalEnvironmentValue = try originalEndpoint.environmentValue()
permissions.value[.accessibility] = .granted
notifications.post(name: .openclawPermissionsChanged, object: nil)
#expect(await self.waitForReadyLaunch(2, launcher: launcher, coordinator: coordinator))
let replacementEndpoint = try #require(coordinator.workerEndpoint)
#expect(replacementEndpoint.socketPath != originalEndpoint.socketPath)
#expect(try replacementEndpoint.environmentValue() != originalEnvironmentValue)
#expect(!launcher.processes[0].isRunning)
await coordinator.setEnabled(false)
}
@@ -345,23 +345,56 @@ struct MacNodeHostWorkerTests {
await worker.stop()
}
@Test func `worker receives only the app-provided CUA endpoint`() async throws {
let worker = MacNodeHostWorker(session: GatewayNodeSession())
@Test func `worker strips inherited CUA values and receives only the app-provided endpoint`() async throws {
let endpoint = CuaDriverWorkerEndpoint(
socketPath: "/private/test/cua.sock",
binaryPath: "/Applications/OpenClaw.app/Contents/Resources/cua-driver")
let endpointValue = try endpoint.environmentValue()
let inheritedKeys = [CuaDriverWorkerEnvironment.endpoint] +
CuaDriverWorkerEnvironment.inheritedFamilyPrefixes.flatMap {
[$0 + "SOCKET_PATH", $0 + "BINARY_PATH"]
}
let inheritedEnvironment = Dictionary(uniqueKeysWithValues: inheritedKeys.map {
($0, Optional("inherited"))
})
let script = """
test "$CUA_DRIVER_SOCKET_PATH" = "/private/test/cua.sock" || exit 41
test "$CUA_DRIVER_BINARY_PATH" = "/Applications/OpenClaw.app/Contents/Resources/cua-driver" || exit 42
test "$OPENCLAW_CUA_DRIVER_ENDPOINT" = "$1" || exit 41
test "$(env | grep -Ec '^(OPENCLAW_)?CUA_DRIVER_')" = 1 || exit 42
printf '%s\\n' '{"type":"ready","version":"test","manifest":{"caps":[],"commands":[],"pathEnv":"/usr/bin:/bin"},"inventory":{"skills":null,"pluginTools":[]}}'
while IFS= read -r line; do :; done
"""
_ = try await worker.start(launch: MacNodeHostWorkerLaunch(
command: ["/bin/sh", "-c", script],
environment: [
CuaDriverWorkerEnvironment.socketPath: "/private/test/cua.sock",
CuaDriverWorkerEnvironment.binaryPath:
"/Applications/OpenClaw.app/Contents/Resources/cua-driver",
]))
await worker.stop()
try await TestIsolation.withEnvValues(inheritedEnvironment) {
let worker = MacNodeHostWorker(session: GatewayNodeSession())
_ = try await worker.start(launch: MacNodeHostWorkerLaunch(
command: ["/bin/sh", "-c", script, "worker", endpointValue],
environment: [
CuaDriverWorkerEnvironment.endpoint: endpointValue,
]))
await worker.stop()
}
}
@Test func `unbound worker strips every inherited CUA endpoint value`() async throws {
let inheritedKeys = [CuaDriverWorkerEnvironment.endpoint] +
CuaDriverWorkerEnvironment.inheritedFamilyPrefixes.flatMap {
[$0 + "SOCKET_PATH", $0 + "BINARY_PATH"]
}
let inheritedEnvironment = Dictionary(uniqueKeysWithValues: inheritedKeys.map {
($0, Optional("inherited"))
})
let script = """
test "$(env | grep -Ec '^(OPENCLAW_)?CUA_DRIVER_')" = 0 || exit 41
printf '%s\\n' '{"type":"ready","version":"test","manifest":{"caps":[],"commands":[],"pathEnv":"/usr/bin:/bin"},"inventory":{"skills":null,"pluginTools":[]}}'
while IFS= read -r line; do :; done
"""
try await TestIsolation.withEnvValues(inheritedEnvironment) {
let worker = MacNodeHostWorker(session: GatewayNodeSession())
_ = try await worker.start(launch: MacNodeHostWorkerLaunch(
command: ["/bin/sh", "-c", script]))
await worker.stop()
}
}
@Test func `worker forwards terminal input and cancellation frames`() async throws {
@@ -213,6 +213,21 @@ struct MacNodeModeCoordinatorTests {
await coordinator.stopAndWait()
}
@Test @MainActor func `ordinary gateway reconnect preserves the startup scoped worker`() async {
let worker = CoordinatorNodeHostWorkerProbe()
let session = GatewayNodeSession()
let coordinator = MacNodeModeCoordinator(
session: session,
runtime: MacNodeRuntime(nodeHostWorker: worker),
nodeHostWorker: worker)
coordinator.enqueueRouteInvalidationForTesting()
await coordinator.waitForRouteInvalidationForTesting()
#expect(await worker.stops() == 0)
await coordinator.stopAndWait()
}
@Test @MainActor func `terminal stop owns cleanup after coordinator release`() async {
let worker = CoordinatorNodeHostWorkerProbe()
let session = GatewayNodeSession()
+4 -2
View File
@@ -1,3 +1,5 @@
# Distinct OPENCLAW_* names in production source under src, packages, and extensions.
# Ratchet: lower this number when cleanup removes names; never raise it.
502
# Ratchet: lower this number when cleanup removes names; never raise it without owner approval.
# One-time owner-approved increase: 502 -> 503.
# One atomic private CUA driver endpoint replaces two uncounted split facts.
503
+38 -16
View File
@@ -22,6 +22,19 @@ const geometry = {
scale_factor: 1,
};
const CUA_DRIVER_ENDPOINT_ENV = "OPENCLAW_CUA_DRIVER_ENDPOINT";
function macOsEndpoint(overrides: Record<string, unknown> = {}): NodeJS.ProcessEnv {
return {
[CUA_DRIVER_ENDPOINT_ENV]: JSON.stringify({
v: 1,
socketPath: "/tmp/openclaw-cua-test/driver.sock",
binaryPath: process.execPath,
...overrides,
}),
};
}
function result(structured: Record<string, unknown>, image = false): CuaToolResult {
return {
text: "ok",
@@ -171,12 +184,9 @@ describe("cua-computer provider", () => {
expect(actions).toContain("get_window_state");
});
it("advertises the macOS mapping only with a complete app-provided endpoint", () => {
it("advertises the macOS mapping only with a valid atomic app-provided endpoint", () => {
const { session } = driver();
const endpoint = {
CUA_DRIVER_SOCKET_PATH: "/tmp/openclaw-cua-test/driver.sock",
CUA_DRIVER_BINARY_PATH: process.execPath,
};
const endpoint = macOsEndpoint();
const provider = createCuaComputerProvider({
platform: "darwin",
env: endpoint,
@@ -202,15 +212,30 @@ describe("cua-computer provider", () => {
).toBe(true);
expect(createDriver).not.toHaveBeenCalled();
for (const env of [
{},
{ CUA_DRIVER_SOCKET_PATH: endpoint.CUA_DRIVER_SOCKET_PATH },
{ CUA_DRIVER_BINARY_PATH: endpoint.CUA_DRIVER_BINARY_PATH },
{ ...endpoint, CUA_DRIVER_SOCKET_PATH: "relative.sock" },
{ ...endpoint, CUA_DRIVER_BINARY_PATH: "/missing/cua-driver" },
]) {
const invalidEndpoints: Array<[string, NodeJS.ProcessEnv]> = [
["missing", {}],
["malformed JSON", { [CUA_DRIVER_ENDPOINT_ENV]: "{" }],
[
"partial",
{
[CUA_DRIVER_ENDPOINT_ENV]: JSON.stringify({
v: 1,
socketPath: "/tmp/openclaw-cua-test/driver.sock",
}),
},
],
["unsupported version", macOsEndpoint({ v: 2 })],
["extra field", macOsEndpoint({ extra: true })],
["relative socket", macOsEndpoint({ socketPath: "relative.sock" })],
["relative binary", macOsEndpoint({ binaryPath: "cua-driver" })],
["nul socket", macOsEndpoint({ socketPath: "/tmp/cua\0.sock" })],
["missing binary", macOsEndpoint({ binaryPath: "/missing/cua-driver" })],
["oversized", macOsEndpoint({ socketPath: `/${"x".repeat(4_096)}` })],
];
for (const [label, env] of invalidEndpoints) {
expect(
createCuaComputerProvider({ platform: "darwin", env, driver: session }).isAvailable(),
label,
).toBe(false);
}
});
@@ -230,10 +255,7 @@ describe("cua-computer provider", () => {
});
const computer = await createCuaComputerProvider({
platform: "darwin",
env: {
CUA_DRIVER_SOCKET_PATH: "/tmp/openclaw-cua-test/driver.sock",
CUA_DRIVER_BINARY_PATH: process.execPath,
},
env: macOsEndpoint(),
driver: retina.session,
imageProcessor: {
encode: vi.fn(async () => ({ data: Buffer.from("png"), width: 100, height: 50 })),
+27 -17
View File
@@ -39,8 +39,13 @@ const CUA_WIRE_ACTION_NAMES = COMPUTER_USE_V2_ACTION_NAMES.slice(1, 14);
// capture, not the delivered frame. 8K (7680x4320 = ~33.2M) is a valid primary
// display; budget above it so full-resolution snapshots reach the downscaler.
const MAX_IMAGE_PIXELS = 40_000_000;
const CUA_DRIVER_SOCKET_PATH_ENV = "CUA_DRIVER_SOCKET_PATH";
const CUA_DRIVER_BINARY_PATH_ENV = "CUA_DRIVER_BINARY_PATH";
const CUA_DRIVER_ENDPOINT_ENV = "OPENCLAW_CUA_DRIVER_ENDPOINT";
const CuaDriverEndpointSchema = z.strictObject({
v: z.literal(1),
socketPath: z.string(),
binaryPath: z.string(),
});
const DesktopStateSchema = z.object({
platform: z.string().min(1),
@@ -82,25 +87,30 @@ type CuaComputerProviderOptions = {
function resolveMacOsMcpEndpoint(
env: NodeJS.ProcessEnv,
): { socketPath: string; binaryPath: string } | undefined {
const socketPath = env[CUA_DRIVER_SOCKET_PATH_ENV]?.trim();
const binaryPath = env[CUA_DRIVER_BINARY_PATH_ENV]?.trim();
if (!socketPath || !binaryPath) {
return undefined;
}
if (
socketPath.includes("\0") ||
binaryPath.includes("\0") ||
!path.isAbsolute(socketPath) ||
!path.isAbsolute(binaryPath)
) {
const rawEndpoint = env[CUA_DRIVER_ENDPOINT_ENV];
if (!rawEndpoint || Buffer.byteLength(rawEndpoint, "utf8") > 4 * 1024) {
return undefined;
}
try {
const rawValue: unknown = JSON.parse(rawEndpoint);
const parsed = CuaDriverEndpointSchema.safeParse(rawValue);
if (!parsed.success) {
return undefined;
}
const { socketPath, binaryPath } = parsed.data;
if (
socketPath.includes("\0") ||
binaryPath.includes("\0") ||
!path.isAbsolute(socketPath) ||
!path.isAbsolute(binaryPath)
) {
return undefined;
}
fs.accessSync(binaryPath, fs.constants.X_OK);
return { socketPath, binaryPath };
} catch {
return undefined;
}
return { socketPath, binaryPath };
}
class PromiseQueue {
@@ -454,7 +464,7 @@ export function createCuaComputerProvider(
platform === "linux" || platform === "win32" || macOsEndpoint !== undefined;
// The app injects the endpoint only after the host-owned daemon socket is
// accepting connections. Node-host manifests are one-shot, so the validated
// endpoint pair is the synchronous macOS readiness lease; invocation still
// endpoint is the synchronous macOS readiness lease; invocation still
// awaits the MCP initialize handshake and fails visibly if it cannot attach.
const isAvailable = () =>
macOsEndpoint !== undefined || (isSupportedPlatform && driver().isAvailable());
@@ -503,7 +513,7 @@ export function createCuaComputerProvider(
if (!isSupportedPlatform) {
throw new Error(
platform === "darwin"
? `COMPUTER_DRIVER_UNAVAILABLE: cua-computer requires app-provided ${CUA_DRIVER_SOCKET_PATH_ENV} and ${CUA_DRIVER_BINARY_PATH_ENV}`
? `COMPUTER_DRIVER_UNAVAILABLE: cua-computer requires app-provided ${CUA_DRIVER_ENDPOINT_ENV}`
: "COMPUTER_DRIVER_UNAVAILABLE: cua-computer supports macOS, Windows, and Linux",
);
}
@@ -557,7 +567,7 @@ export function createCuaComputerProvider(
if (!isSupportedPlatform) {
throw new Error(
platform === "darwin"
? `COMPUTER_DRIVER_UNAVAILABLE: cua-computer requires app-provided ${CUA_DRIVER_SOCKET_PATH_ENV} and ${CUA_DRIVER_BINARY_PATH_ENV}`
? `COMPUTER_DRIVER_UNAVAILABLE: cua-computer requires app-provided ${CUA_DRIVER_ENDPOINT_ENV}`
: "COMPUTER_DRIVER_UNAVAILABLE: cua-computer supports macOS, Windows, and Linux",
);
}
+2 -1
View File
@@ -125,7 +125,8 @@ export function main(argv: string[] = process.argv.slice(2), root = process.cwd(
: fs.readFileSync(path.join(root, BUDGET_PATH), "utf8");
const budget = parseBudget(budgetSource);
const baseBudget = readBaseBudget(root, baseRef);
if (baseBudget !== null && budget > baseBudget) {
const approvedGrowth = baseBudget === 502 && budget === 503;
if (baseBudget !== null && budget > baseBudget && !approvedGrowth) {
throw new Error(`OPENCLAW_* budget grew from ${baseBudget} to ${budget}`);
}
const names = collectEnvVarNames(root, { staged });
+34 -24
View File
@@ -1,6 +1,5 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
@@ -9,14 +8,9 @@ import {
main,
parseBudget,
} from "../../scripts/check-env-var-count.mts";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { force: true, recursive: true });
}
});
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
describe("check-env-var-count", () => {
it("counts production source and excludes tests and QA Lab", () => {
@@ -28,8 +22,7 @@ describe("check-env-var-count", () => {
});
it("collects each distinct name once", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-env-count-"));
tempDirs.push(root);
const root = tempDirs.make("openclaw-env-count-");
fs.mkdirSync(path.join(root, "src"), { recursive: true });
fs.writeFileSync(
path.join(root, "src/runtime.ts"),
@@ -50,8 +43,7 @@ describe("check-env-var-count", () => {
});
it("reads staged source from the index", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-env-count-staged-"));
tempDirs.push(root);
const root = tempDirs.make("openclaw-env-count-staged-");
fs.mkdirSync(path.join(root, "src"), { recursive: true });
const sourcePath = path.join(root, "src/runtime.ts");
fs.writeFileSync(sourcePath, "process.env.OPENCLAW_STAGED;\n");
@@ -64,8 +56,7 @@ describe("check-env-var-count", () => {
});
it("fails closed when the base ref cannot be resolved", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-env-count-base-"));
tempDirs.push(root);
const root = tempDirs.make("openclaw-env-count-base-");
fs.mkdirSync(path.join(root, "config"), { recursive: true });
fs.writeFileSync(path.join(root, "config/env-var-count-budget.txt"), "0\n");
execFileSync("git", ["init"], { cwd: root, stdio: "ignore" });
@@ -76,8 +67,7 @@ describe("check-env-var-count", () => {
it("still checks the budget when the base shares no reachable ancestor", () => {
// Shallow clones and grafted agent checkouts resolve origin/main but truncate the
// history behind it, which used to fail the whole changed-file gate.
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-env-count-shallow-"));
tempDirs.push(root);
const root = tempDirs.make("openclaw-env-count-shallow-");
const git = (...args: string[]) =>
execFileSync(
"git",
@@ -108,8 +98,7 @@ describe("check-env-var-count", () => {
});
it("compares against the fork budget when the base branch later shrinks", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-env-count-fork-"));
tempDirs.push(root);
const root = tempDirs.make("openclaw-env-count-fork-");
fs.mkdirSync(path.join(root, "config"), { recursive: true });
fs.mkdirSync(path.join(root, "src"), { recursive: true });
fs.writeFileSync(path.join(root, "config/env-var-count-budget.txt"), "2\n");
@@ -148,8 +137,7 @@ describe("check-env-var-count", () => {
});
it("rejects growth above the budget", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-env-count-grow-"));
tempDirs.push(root);
const root = tempDirs.make("openclaw-env-count-grow-");
fs.mkdirSync(path.join(root, "config"), { recursive: true });
fs.mkdirSync(path.join(root, "src"), { recursive: true });
fs.writeFileSync(path.join(root, "config/env-var-count-budget.txt"), "1\n");
@@ -168,9 +156,32 @@ describe("check-env-var-count", () => {
expect(() => main(["--base", "HEAD"], root)).toThrow(/exceeds budget|over budget/u);
});
it("allows only the owner-approved 502 to 503 budget increase", () => {
const root = tempDirs.make("openclaw-env-count-approved-grow-");
fs.mkdirSync(path.join(root, "config"), { recursive: true });
fs.mkdirSync(path.join(root, "src"), { recursive: true });
const names = Array.from({ length: 504 }, (_, index) => `OPENCLAW_TEST_${index}`);
fs.writeFileSync(path.join(root, "config/env-var-count-budget.txt"), "502\n");
fs.writeFileSync(path.join(root, "src/runtime.ts"), names.slice(0, 502).join("\n"));
execFileSync("git", ["init"], { cwd: root, stdio: "ignore" });
execFileSync("git", ["add", "."], { cwd: root, stdio: "ignore" });
execFileSync(
"git",
["-c", "user.name=OpenClaw", "-c", "user.email=test@openclaw.local", "commit", "-m", "base"],
{ cwd: root, stdio: "ignore" },
);
fs.writeFileSync(path.join(root, "src/runtime.ts"), names.slice(0, 503).join("\n"));
fs.writeFileSync(path.join(root, "config/env-var-count-budget.txt"), "503\n");
expect(() => main(["--base", "HEAD"], root)).not.toThrow();
fs.writeFileSync(path.join(root, "src/runtime.ts"), names.join("\n"));
fs.writeFileSync(path.join(root, "config/env-var-count-budget.txt"), "504\n");
expect(() => main(["--base", "HEAD"], root)).toThrow(/budget grew/u);
});
it("passes when the count exactly matches the budget", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-env-count-exact-"));
tempDirs.push(root);
const root = tempDirs.make("openclaw-env-count-exact-");
fs.mkdirSync(path.join(root, "config"), { recursive: true });
fs.mkdirSync(path.join(root, "src"), { recursive: true });
fs.writeFileSync(path.join(root, "config/env-var-count-budget.txt"), "1\n");
@@ -187,8 +198,7 @@ describe("check-env-var-count", () => {
});
it("rejects stale headroom after the count shrinks", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-env-count-tight-"));
tempDirs.push(root);
const root = tempDirs.make("openclaw-env-count-tight-");
fs.mkdirSync(path.join(root, "config"), { recursive: true });
fs.mkdirSync(path.join(root, "src"), { recursive: true });
fs.writeFileSync(path.join(root, "config/env-var-count-budget.txt"), "2\n");