1import { assert, describe, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Schema from "effect/Schema";
6import * as Sink from "effect/Sink";
7import * as Stream from "effect/Stream";
8import { ChildProcessSpawner } from "effect/process";
9
10import * as HostProcess from "./HostProcess.ts";
11
12import {
13 MACOS_MANAGED_PREFERENCES_PATH,
14 managedPolicyPath,
15 readManagedPolicy,
16 type RegistryKey,
17 WINDOWS_POLICY_KEY,
18 layerWindowsRegistry,
19 WindowsRegistry,
20 WindowsRegistryError,
21} from "./managedPolicyFile.ts";
22
23const policy = JSON.stringify({ schemaVersion: 1, organization: "Example Corp" });
24
25/** What a command prints and exits with. */
26type Result = { readonly stdout: string; readonly exitCode: number };
27
28/** Keys below the policy key ("" for the key itself), or the error every read fails with. */
29type RegistryAnswer = Readonly<Record<string, RegistryKey>> | WindowsRegistryError;
30const isRegistryError = Schema.is(WindowsRegistryError);
31const POLICY_SUBKEY = WINDOWS_POLICY_KEY.slice("HKLM\\".length);
32
33/** Files that exist, what a command prints and exits with by command name, and the registry. */
34const machine = (
35 files: Readonly<Record<string, string>>,
36 commands: Readonly<Record<string, Result>>,
37 registry: RegistryAnswer,
38) =>
39 Layer.mergeAll(
40 Layer.succeed(
41 WindowsRegistry,
42 WindowsRegistry.of({
43 readMachineKey: (subkey) =>
44 isRegistryError(registry)
45 ? Effect.fail(registry)
46 : Effect.succeed(registry[subkey.slice(POLICY_SUBKEY.length + 1)]),
47 }),
48 ),
49 FileSystem.layerNoop({
50 exists: (path) => Effect.succeed(path in files),
51 readFileString: (path) => Effect.succeed(files[path] ?? ""),
52 }),
53 Layer.succeed(
54 ChildProcessSpawner.ChildProcessSpawner,
55 ChildProcessSpawner.make((command) => {
56 const { command: name } = command as unknown as { readonly command: string };
57 const result = commands[name] ?? { stdout: "", exitCode: 127 };
58 return Effect.succeed(
59 ChildProcessSpawner.makeHandle({
60 pid: ChildProcessSpawner.ProcessId(1),
61 exitCode: Effect.succeed(ChildProcessSpawner.ExitCode(result.exitCode)),
62 isRunning: Effect.succeed(false),
63 kill: () => Effect.void,
64 unref: Effect.succeed(Effect.void),
65 stdin: Sink.drain,
66 stdout: Stream.make(new TextEncoder().encode(result.stdout)),
67 stderr: Stream.empty,
68 all: Stream.empty,
69 getInputFd: () => Sink.drain,
70 getOutputFd: () => Stream.empty,
71 }),
72 );
73 }),
74 ),
75 );
76
77/** A policy key holding `values`, with checkbox subkeys of `name=0|1` entries. */
78const policyKey = (
79 values: RegistryKey["values"],
80 subkeys: Readonly<Record<string, Readonly<Record<string, number>>>> = {},
81): Readonly<Record<string, RegistryKey>> => ({
82 "": { values, subkeys: Object.keys(subkeys) },
83 ...Object.fromEntries(
84 Object.entries(subkeys).map(([name, boxes]) => [
85 name,
86 {
87 values: Object.entries(boxes).map(([box, data]) => ({
88 name: box,
89 type: "REG_DWORD" as const,
90 data,
91 })),
92 subkeys: [],
93 },
94 ]),
95 ),
96});
97const text = (name: string, data: string) => ({ name, type: "REG_SZ" as const, data });
98const dword = (name: string, data: number) => ({ name, type: "REG_DWORD" as const, data });
99
100const read = (
101 platform: NodeJS.Platform,
102 files: Readonly<Record<string, string>>,
103 commands: Parameters<typeof machine>[1] = {},
104 registry: RegistryAnswer = {},
105) =>
106 readManagedPolicy(platform).pipe(
107 Effect.map((found) => found?.source),
108 Effect.catchTags({
109 ManagedPolicyFileError: (error) => Effect.succeed(`error: ${error.reason}`),
110 }),
111 Effect.provide(machine(files, commands, registry)),
112 );
113
114describe("readManagedPolicy", () => {
115 it.effect("reads a macOS configuration profile, and refuses one beside a policy file", () =>
116 Effect.gen(function* () {
117 const plutil = { "/usr/bin/plutil": { stdout: policy, exitCode: 0 } };
118 const profile = { [MACOS_MANAGED_PREFERENCES_PATH]: "<binary plist>" };
119 const file = { [managedPolicyPath("darwin")]: policy };
120
121 assert.equal(yield* read("darwin", profile, plutil), MACOS_MANAGED_PREFERENCES_PATH);
122 const plist = (contents: object) => ({
123 "/usr/bin/plutil": { stdout: JSON.stringify(contents), exitCode: 0 },
124 });
125 const settings = JSON.parse(policy);
126 // What macOS 27 writes for a manually installed profile.
127 assert.equal(
128 yield* read(
129 "darwin",
130 profile,
131 plist({ ...settings, PayloadUUID: "e8c7f49b", _manualProfile: true }),
132 ),
133 MACOS_MANAGED_PREFERENCES_PATH,
134 );
135 assert.equal(
136 yield* read("darwin", profile, plist({ Forced: [{ mcx_preference_settings: settings }] })),
137 MACOS_MANAGED_PREFERENCES_PATH,
138 );
139 // Keys beside the wrapper, or a second entry, are refused rather than dropped or merged.
140 for (const ambiguous of [
141 { Forced: [{ mcx_preference_settings: settings }], limits: { telemetry: false } },
142 { Forced: [{ mcx_preference_settings: settings }, { mcx_preference_settings: settings }] },
143 ]) {
144 assert.include(
145 yield* read("darwin", profile, plist(ambiguous)),
146 "does not match the policy schema",
147 );
148 }
149 assert.equal(yield* read("darwin", file), managedPolicyPath("darwin"));
150 const typo = JSON.stringify({ ...settings, limits: { maxRuntimeMod: "auto" } });
151 assert.include(
152 yield* read("darwin", { [managedPolicyPath("darwin")]: typo }),
153 '["limits"]["maxRuntimeMod"]',
154 );
155 assert.isUndefined(yield* read("darwin", {}));
156 assert.include(
157 yield* read("darwin", { ...profile, ...file }, plutil),
158 "deploy the policy one way",
159 );
160 // A profile plutil cannot convert fails closed rather than reading as no policy.
161 assert.include(
162 yield* read("darwin", profile, { "/usr/bin/plutil": { stdout: "", exitCode: 1 } }),
163 "cannot be read",
164 );
165 }),
166 );
167
168 it.effect(
169 "reads a Policy value of JSON from the registry, and treats only an absent key as none",
170 () =>
171 Effect.gen(function* () {
172 const file = { [managedPolicyPath("win32")]: policy };
173 const win32 = (files: Readonly<Record<string, string>>, registry: RegistryAnswer) =>
174 read("win32", files, {}, registry);
175 const json = policyKey([text("Policy", policy)]);
176
177 assert.equal(yield* win32({}, json), WINDOWS_POLICY_KEY);
178 assert.isUndefined(yield* win32({}, {}));
179 assert.equal(yield* win32(file, {}), managedPolicyPath("win32"));
180 assert.include(yield* win32(file, json), "deploy the policy one way");
181 // A read that fails, here access denied, is not read as no policy, file or not.
182 for (const files of [{}, file]) {
183 assert.include(
184 yield* win32(files, new WindowsRegistryError({ code: 5 })),
185 "the registry cannot be read (Windows error 5)",
186 );
187 }
188 assert.include(
189 yield* win32({}, new WindowsRegistryError({})),
190 "cannot load its registry reader; reinstall T3 Code",
191 );
192 assert.include(
193 yield* win32({}, policyKey([{ name: "Policy", type: "other" }])),
194 "its Policy value is not a string (REG_SZ)",
195 );
196 assert.include(
197 yield* win32({}, policyKey([text("Policy", "{ not json")])),
198 "does not match the policy schema",
199 );
200 for (const both of [
201 policyKey([text("Policy", policy), text("Organization", "Example")]),
202 policyKey([text("Policy", policy)], { Clients: { desktop: 1 } }),
203 ]) {
204 assert.include(yield* win32({}, both), "both a Policy value and Group Policy settings");
205 }
206 // Not Configured clears a list policy's values but can leave its empty subkey behind.
207 assert.equal(
208 yield* win32({}, policyKey([text("Policy", policy)], { Clients: {} })),
209 WINDOWS_POLICY_KEY,
210 );
211 }),
212 );
213
214 it.effect("builds the policy from the values the Group Policy template writes", () =>
215 Effect.gen(function* () {
216 const organization = text("Organization", "Société 日本 ✓");
217 const document = (registry: RegistryAnswer) =>
218 readManagedPolicy("win32", ["codex", "claudeAgent"]).pipe(
219 Effect.map((found): unknown => found?.document),
220 Effect.catchTags({
221 ManagedPolicyFileError: (error) => Effect.succeed(`error: ${error.reason}`),
222 }),
223 Effect.provide(machine({}, {}, registry)),
224 );
225
226 assert.deepStrictEqual(
227 yield* document(
228 policyKey(
229 [
230 organization,
231 text("SupportUrl", ""),
232 // Registry names ignore case.
233 text("maxRuntimeMode", "auto-accept-edits"),
234 dword("Telemetry", 0),
235 text("Settings", '{"enableAgentBrowserAccess": {"value": false, "locked": true}}'),
236 text("UpdatesMode", "off"),
237 dword("AllowNightly", 1),
238 dword("Updates", 1),
239 dword("Clients", 1),
240 dword("AllowedDrivers", 1),
241 ],
242 // Registry names ignore case, so a subkey written in other case still counts.
243 {
244 clients: { Desktop: 1, "LOCAL-BROWSER": 1, REMOTE: 1 },
245 AllowedDrivers: { Codex: 1, CLAUDEAGENT: 1, UnknownDriver: 1 },
246 },
247 ),
248 ),
249 {
250 schemaVersion: 1,
251 organization: "Société 日本 ✓",
252 settings: { enableAgentBrowserAccess: { value: false, locked: true } },
253 limits: {
254 maxRuntimeMode: "auto-accept-edits",
255 telemetry: false,
256 updates: { mode: "off", allowNightly: true },
257 clients: ["desktop", "local-browser", "remote"],
258 providers: { allowedDrivers: ["codex", "claudeAgent", "UnknownDriver"] },
259 },
260 },
261 );
262
263 // Disabling a policy leaves each checkbox's falseValue behind; it restricts nothing.
264 const disabledGroups = {
265 Clients: { desktop: 0, "local-browser": 0, remote: 0 },
266 AllowedDrivers: { codex: 0, claudeAgent: 0 },
267 };
268 assert.deepStrictEqual(
269 yield* document(
270 policyKey(
271 [
272 organization,
273 dword("Clients", 0),
274 dword("AllowedDrivers", 0),
275 dword("Updates", 0),
276 dword("AllowNightly", 0),
277 dword("Telemetry", 0),
278 ],
279 disabledGroups,
280 ),
281 ),
282 { schemaVersion: 1, organization: "Société 日本 ✓", limits: { telemetry: false } },
283 );
284 // With nothing else configured, that is no policy rather than one missing Organization.
285 assert.isUndefined(
286 yield* document(
287 policyKey([dword("AllowedDrivers", 0)], {
288 AllowedDrivers: disabledGroups.AllowedDrivers,
289 }),
290 ),
291 );
292 assert.isUndefined(yield* document(policyKey([])));
293
294 // Anything the template does not write fails closed rather than being ignored.
295 for (const [registry, reason] of [
296 [policyKey([organization, dword("Telemetry", 2)]), "DWORD of 0 or 1"],
297 [policyKey([organization, text("MaxMode", "auto")]), "not a T3 Code policy value"],
298 [policyKey([organization], { Other: { x: 1 } }), "Other is not a T3 Code policy key"],
299 [
300 {
301 ...policyKey([organization, dword("Clients", 1)], { Clients: { desktop: 1 } }),
302 Clients: { values: [dword("desktop", 1)], subkeys: ["Nested"] },
303 },
304 "Clients\\Nested is not a T3 Code policy key",
305 ],
306 [policyKey([organization, text("Settings", "{")]), "not valid JSON"],
307 [policyKey([organization, dword("Clients", 2)]), "DWORD of 0 or 1"],
308 [
309 policyKey([organization, dword("Clients", 1)], { Clients: { UnknownClient: 1 } }),
310 "does not match the policy schema",
311 ],
312 [
313 policyKey([organization, dword("Clients", 1)], { Clients: { desktop: 0 } }),
314 "does not match the policy schema",
315 ],
316 [policyKey([dword("Organization", 1)]), "Organization must be a string"],
317 // REG_EXPAND_SZ would fill in the reading user's variables.
318 [policyKey([{ name: "Organization", type: "other" }]), "Organization must be a string"],
319 [
320 policyKey([text("MaxRuntimeMode", "auto-accept-edits")]),
321 "set the Organization policy whenever another T3 Code policy is set",
322 ],
323 ] as const) {
324 assert.include(yield* document(registry), reason);
325 }
326 }),
327 );
328
329 it.effect.runIf(HostProcess.Platform.defaultValue() === "win32")(
330 "lists registry values and subkeys through the Win32 API",
331 () =>
332 Effect.gen(function* () {
333 const registry = yield* WindowsRegistry;
334 const version = yield* registry.readMachineKey(
335 "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion",
336 );
337 const value = (name: string) => version?.values.find((entry) => entry.name === name);
338 const product = value("ProductName");
339 assert.match(product?.type === "REG_SZ" ? product.data : "", /Windows/);
340 assert.equal(value("CurrentMajorVersionNumber")?.type, "REG_DWORD");
341 assert.isUndefined(yield* registry.readMachineKey("SOFTWARE\\T3CodeNoSuchKey"));
342 const windows = yield* registry.readMachineKey(
343 "SOFTWARE\\Microsoft\\Windows\\CurrentVersion",
344 );
345 // REG_EXPAND_SZ, which would otherwise expand the user's variables.
346 assert.equal(
347 windows?.values.find((entry) => entry.name === "ProgramFilesPath")?.type,
348 "other",
349 );
350 const parent = yield* registry.readMachineKey("SOFTWARE\\Microsoft\\Windows NT");
351 assert.isTrue(parent?.subkeys.includes("CurrentVersion"));
352 }).pipe(Effect.provide(layerWindowsRegistry(() => import("ffi-rs")))),
353 );
354});