Skip to content
Source
Sign inSign up

managedPolicy.ts

244 lines10.7 KB

1import * as Schema from "effect/Schema";
2import * as Struct from "effect/Struct";
3​
4import { TrimmedNonEmptyString } from "./baseSchemas.ts";
5import { ProviderDriverKind, ProviderInstanceEnvironmentVariableName } from "./providerInstance.ts";
6import { isRuntimeModeAbove, RuntimeMode } from "./providerPolicy.ts";
7​
8/**
9 * One server setting an administrator manages, keyed in the policy by its dot path in the
10 * encoded `ServerSettings` (for example `storageCleanup.logsAfterDays`). `locked: false`
11 * replaces the built-in default; `locked: true` also stops users and agents from changing it.
12 */
13export const ManagedPolicySetting = Schema.Struct({
14 value: Schema.Unknown,
15 locked: Schema.Boolean,
16});
17export type ManagedPolicySetting = typeof ManagedPolicySetting.Type;
18​
19/**
20 * Fields an administrator pins on every instance of one driver, merged over the user's instance
21 * when settings are read. `binaryPath` and `launchArgs` land in the driver's `config`, so a driver
22 * without that config field rejects the policy. `locked: false` fills only what the user left empty.
23 */
24export const ManagedProviderInstance = Schema.Struct({
25 locked: Schema.Boolean,
26 environment: Schema.optionalKey(
27 Schema.Array(
28 Schema.Struct({ name: ProviderInstanceEnvironmentVariableName, value: Schema.String }),
29 ),
30 ),
31 binaryPath: Schema.optionalKey(TrimmedNonEmptyString),
32 launchArgs: Schema.optionalKey(TrimmedNonEmptyString),
33});
34export type ManagedProviderInstance = typeof ManagedProviderInstance.Type;
35​
36export const ManagedProviderLimits = Schema.Struct({
37 /** Only these drivers run; instances of any other driver show as unavailable. */
38 allowedDrivers: Schema.optionalKey(Schema.NonEmptyArray(ProviderDriverKind)),
39 instances: Schema.optionalKey(Schema.Record(ProviderDriverKind, ManagedProviderInstance)),
40});
41export type ManagedProviderLimits = typeof ManagedProviderLimits.Type;
42​
43/**
44 * How T3 Code updates itself. `auto` is the default: it checks, and installs only when a user
45 * asks. `off` never checks, for machines that get T3 Code through MDM.
46 */
47export const ManagedUpdatesMode = Schema.Literals(["auto", "off"]);
48export type ManagedUpdatesMode = typeof ManagedUpdatesMode.Type;
49​
50const HttpsUrl = TrimmedNonEmptyString.check(
51 Schema.makeFilter((value: string) => /^https:\/\/[^\s/]+/i.test(value.trim()), {
52 expected: "an https URL",
53 }),
54);
55​
56export const ManagedUpdatesPolicy = Schema.Struct({
57 mode: ManagedUpdatesMode,
58 /** A generic electron-updater feed the desktop app updates from instead of GitHub. */
59 feedUrl: Schema.optionalKey(HttpsUrl),
60 /** `false` makes the nightly channel unavailable. */
61 allowNightly: Schema.optionalKey(Schema.Boolean),
62});
63export type ManagedUpdatesPolicy = typeof ManagedUpdatesPolicy.Type;
64​
65/** A kind of client the server may accept, mapped onto `ServerAuthPolicy`. */
66export const ManagedPolicyClient = Schema.Literals(["desktop", "local-browser", "remote"]);
67export type ManagedPolicyClient = typeof ManagedPolicyClient.Type;
68​
69/**
70 * Restrictions that are not settings: users cannot opt out of them anywhere. For the boolean
71 * capabilities, omitted means allowed and only `false` restricts.
72 */
73export const ManagedPolicyLimits = Schema.Struct({
74 /** The broadest runtime mode any thread may be created in, switched to, or run turns in. */
75 maxRuntimeMode: Schema.optionalKey(RuntimeMode),
76 /** Clients the server accepts. Without `remote`, nothing off this machine can pair. */
77 clients: Schema.optionalKey(Schema.NonEmptyArray(ManagedPolicyClient)),
78 /** `false` turns T3 Connect off and unlinks this machine. */
79 connect: Schema.optionalKey(Schema.Boolean),
80 webhookTasks: Schema.optionalKey(Schema.Boolean),
81 /** Can only turn product analytics off, never force it on. */
82 telemetry: Schema.optionalKey(Schema.Boolean),
83 browserCookieImport: Schema.optionalKey(Schema.Boolean),
84 providers: Schema.optionalKey(ManagedProviderLimits),
85 updates: Schema.optionalKey(ManagedUpdatesPolicy),
86});
87export type ManagedPolicyLimits = typeof ManagedPolicyLimits.Type;
88​
89/** The limits clients see: what the server enforces, which can be broader than the file says. */
90export const ManagedPolicySummaryLimits = Schema.Struct({
91 maxRuntimeMode: Schema.optionalKey(RuntimeMode),
92 clients: Schema.optionalKey(Schema.NonEmptyArray(ManagedPolicyClient)),
93 /** `false` also when `clients` leaves out `remote`, since Connect is remote access. */
94 connect: Schema.optionalKey(Schema.Boolean),
95 webhookTasks: Schema.optionalKey(Schema.Boolean),
96 telemetry: Schema.optionalKey(Schema.Boolean),
97 browserCookieImport: Schema.optionalKey(Schema.Boolean),
98 providers: Schema.optionalKey(
99 Schema.Struct({
100 /** Absent means every driver is allowed. */
101 allowedDrivers: Schema.optionalKey(Schema.NonEmptyArray(ProviderDriverKind)),
102 /**
103 * Per driver, the instance fields users cannot change, as dot paths into the instance
104 * envelope: `config.binaryPath`, `config.launchArgs`, `environment.<NAME>`. A bare
105 * `environment` means every variable, as under `maxRuntimeMode`, where each driver's
106 * launch configuration belongs to the administrator.
107 */
108 lockedInstanceFields: Schema.optionalKey(
109 Schema.Record(ProviderDriverKind, Schema.Array(Schema.String)),
110 ),
111 }),
112 ),
113 /** No `feedUrl`: it can carry a token, and only the desktop app, which reads the file, uses it. */
114 updates: Schema.optionalKey(ManagedUpdatesPolicy.mapFields(Struct.omit(["feedUrl"]))),
115});
116export type ManagedPolicySummaryLimits = typeof ManagedPolicySummaryLimits.Type;
117​
118/**
119 * The machine-wide policy file an administrator deploys. Decoded with excess properties
120 * rejected: a key this build does not know might be a restriction it cannot enforce, and
121 * starting anyway would look managed without being managed.
122 */
123export const ManagedPolicyDocument = Schema.Struct({
124 schemaVersion: Schema.Literal(1),
125 /** Shown to users as "Managed by <organization>". */
126 organization: TrimmedNonEmptyString,
127 supportUrl: Schema.optionalKey(TrimmedNonEmptyString),
128 settings: Schema.optionalKey(Schema.Record(Schema.String, ManagedPolicySetting)),
129 limits: Schema.optionalKey(ManagedPolicyLimits),
130});
131export type ManagedPolicyDocument = typeof ManagedPolicyDocument.Type;
132​
133/** What clients learn about the policy: enough to explain a locked control, no values. */
134export const ManagedPolicySummary = Schema.Struct({
135 organization: TrimmedNonEmptyString,
136 supportUrl: Schema.optionalKey(TrimmedNonEmptyString),
137 /** Dot paths into `ServerSettings` that users cannot change. */
138 lockedSettings: Schema.Array(Schema.String),
139 /** What the server enforces; a missing key means no limit. */
140 limits: Schema.optionalKey(ManagedPolicySummaryLimits),
141});
142export type ManagedPolicySummary = typeof ManagedPolicySummary.Type;
143​
144/** The message every surface shows when the policy stops an update. */
145export function managedUpdatesMessage(organization: string): string {
146 return `${organization} manages T3 Code updates.`;
148​
149/** The message when the policy allows only stable releases. */
150export function managedNightlyDeniedMessage(organization: string): string {
151 return `${managedUpdatesMessage(organization)} Only stable releases can be installed.`;
153​
154/** Whether `path` is locked itself, sits inside a locked setting, or contains one. */
155export function isManagedSettingPath(
156 summary: Pick<ManagedPolicySummary, "lockedSettings"> | undefined,
157 path: string,
158): boolean {
159 if (summary === undefined) return false;
160 return summary.lockedSettings.some(
161 (locked) => locked === path || locked.startsWith(`${path}.`) || path.startsWith(`${locked}.`),
162 );
164​
165/** The one shape users see when policy refuses something: "Example Corp's policy does not allow webhook tasks." */
166export function managedPolicyDeniedMessage(organization: string, capability: string): string {
167 return `${organization}'s policy does not allow ${capability}.`;
169​
170const RUNTIME_MODE_LABELS: Record<RuntimeMode, string> = {
171 "approval-required": "Supervised",
172 "auto-accept-edits": "Auto-accept edits",
173 auto: "Auto",
174 "full-access": "Full access",
175};
176​
177/** "Example Corp limits threads to Auto-accept edits." */
178export function managedRuntimeModeCeilingMessage(organization: string, ceiling: RuntimeMode) {
179 return `${organization} limits threads to ${RUNTIME_MODE_LABELS[ceiling]}.`;
181​
182/** Why `mode` is unavailable on an environment with `summary`, or undefined when it is allowed. */
183export function managedRuntimeModeDenial(
184 summary: ManagedPolicySummary | undefined,
185 mode: RuntimeMode,
186): string | undefined {
187 const ceiling = summary?.limits?.maxRuntimeMode;
188 return summary !== undefined && ceiling !== undefined && isRuntimeModeAbove(mode, ceiling)
189 ? managedRuntimeModeCeilingMessage(summary.organization, ceiling)
190 : undefined;
192​
193/**
194 * `mode` when the policy allows it, else the broadest of `modes` (narrowest first) that it does:
195 * where a thread or task above the ceiling switches down to. Undefined when none is allowed.
196 */
197export function clampManagedRuntimeMode(
198 summary: ManagedPolicySummary | undefined,
199 mode: RuntimeMode,
200 modes: ReadonlyArray<RuntimeMode> = RuntimeMode.literals,
201): RuntimeMode | undefined {
202 const ceiling = summary?.limits?.maxRuntimeMode;
203 if (!isRuntimeModeAbove(mode, ceiling)) return mode;
204 return modes.findLast((candidate) => !isRuntimeModeAbove(candidate, ceiling));
206​
207const CAPABILITY_LABELS = {
208 remoteClients: "connecting from other devices",
209 connect: "T3 Connect",
210 webhookTasks: "webhook tasks",
211 browserCookieImport: "importing browser cookies",
212} as const;
213​
214/** Why a capability is unavailable on an environment with `summary`, or undefined when allowed. */
215export function managedCapabilityDenial(
216 summary: ManagedPolicySummary | undefined,
217 capability: keyof typeof CAPABILITY_LABELS,
218): string | undefined {
219 const limits = summary?.limits;
220 if (summary === undefined || limits === undefined) return undefined;
221 const denied =
222 capability === "remoteClients"
223 ? limits.clients !== undefined && !limits.clients.includes("remote")
224 : limits[capability] === false;
225 return denied
226 ? managedPolicyDeniedMessage(summary.organization, CAPABILITY_LABELS[capability])
227 : undefined;
229​
230/** Whether the policy lets `driver` run; clients name it with their own provider label. */
231export function isManagedDriverAllowed(
232 summary: ManagedPolicySummary | undefined,
233 driver: ProviderDriverKind,
234): boolean {
235 const allowed = summary?.limits?.providers?.allowedDrivers;
236 return allowed === undefined || allowed.includes(driver);
238​
239/** Why updates are unavailable on an environment with `summary`, or undefined when allowed. */
240export function managedUpdatesDenial(summary: ManagedPolicySummary | undefined) {
241 return summary?.limits?.updates?.mode === "off"
242 ? managedUpdatesMessage(summary.organization)
243 : undefined;