claude-use 4.4.0 → 5.0.0
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.
- package/README.md +3 -0
- package/dist/cli.cjs +3382 -2595
- package/dist/index.cjs +3061 -0
- package/dist/index.mjs +2991 -0
- package/dist/types/cliError.d.ts +27 -0
- package/dist/types/config/classify.d.ts +49 -0
- package/dist/types/config/load.d.ts +42 -0
- package/dist/types/config/schema.d.ts +689 -0
- package/dist/types/config/store.d.ts +46 -0
- package/dist/types/frontdoor/capability.d.ts +19 -0
- package/dist/types/frontdoor/connect.d.ts +179 -0
- package/dist/types/frontdoor/ensure.d.ts +34 -0
- package/dist/types/frontdoor/probe.d.ts +13 -0
- package/dist/types/frontdoor/state.d.ts +50 -0
- package/dist/types/frontdoor/supervisor.d.ts +48 -0
- package/dist/types/headroom/ensure.d.ts +31 -0
- package/dist/types/headroom/state.d.ts +71 -0
- package/dist/types/headroom/supervisor.d.ts +102 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/launcher/farm.d.ts +165 -0
- package/dist/types/launcher/guard.d.ts +57 -0
- package/dist/types/launcher/lock.d.ts +41 -0
- package/dist/types/launcher/ports.d.ts +133 -0
- package/dist/types/paths.d.ts +69 -0
- package/dist/types/resolve/conditions.d.ts +37 -0
- package/dist/types/resolve/decide.d.ts +45 -0
- package/dist/types/resolve/extends.d.ts +41 -0
- package/dist/types/resolve/flatten.d.ts +13 -0
- package/dist/types/resolve/match.d.ts +58 -0
- package/dist/types/resolve/pipeline.d.ts +30 -0
- package/dist/types/resolve/plan.d.ts +57 -0
- package/dist/types/resolve/projects.d.ts +61 -0
- package/dist/types/resolve/reconcile.d.ts +80 -0
- package/dist/types/resolve/types.d.ts +159 -0
- package/dist/types/resolve/walk.d.ts +85 -0
- package/package.json +16 -2
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2991 @@
|
|
|
1
|
+
// src/config/classify.ts
|
|
2
|
+
import picomatch from "picomatch";
|
|
3
|
+
|
|
4
|
+
// src/config/schema.ts
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
var CATEGORY_NAMES = ["secret", "runtime", "history", "knowledge", "settings"];
|
|
8
|
+
var OVERRIDABLE_CATEGORIES = ["runtime", "history", "knowledge", "settings"];
|
|
9
|
+
function isOverridableCategory(name) {
|
|
10
|
+
return OVERRIDABLE_CATEGORIES.some((category) => category === name);
|
|
11
|
+
}
|
|
12
|
+
function isCategoryName(name) {
|
|
13
|
+
return CATEGORY_NAMES.some((category) => category === name);
|
|
14
|
+
}
|
|
15
|
+
function expandAllCategoryKey(pairs) {
|
|
16
|
+
const { all, ...rest } = pairs;
|
|
17
|
+
if (all === void 0) {
|
|
18
|
+
return { ...rest };
|
|
19
|
+
}
|
|
20
|
+
const expanded = Object.fromEntries(OVERRIDABLE_CATEGORIES.map((category) => [category, all]));
|
|
21
|
+
return { ...expanded, ...rest };
|
|
22
|
+
}
|
|
23
|
+
var CategoryMapSchema = z.strictObject({
|
|
24
|
+
all: z.boolean().optional(),
|
|
25
|
+
runtime: z.boolean().optional(),
|
|
26
|
+
history: z.boolean().optional(),
|
|
27
|
+
knowledge: z.boolean().optional(),
|
|
28
|
+
settings: z.boolean().optional()
|
|
29
|
+
}).transform((input) => expandAllCategoryKey(input));
|
|
30
|
+
var DURATION_RE = /^(?:0|[1-9][0-9]*)(?:ms|s|m|h|d|w)$/;
|
|
31
|
+
var DurationSchema = z.string().regex(DURATION_RE);
|
|
32
|
+
var WhenSchema = z.strictObject({
|
|
33
|
+
newerThan: DurationSchema.optional(),
|
|
34
|
+
olderThan: DurationSchema.optional(),
|
|
35
|
+
maxSizeBytes: z.int().positive().optional(),
|
|
36
|
+
branch: z.string().min(1).optional(),
|
|
37
|
+
env: z.record(z.string().min(1), z.string()).optional()
|
|
38
|
+
});
|
|
39
|
+
var EntryValueSchema = z.union([
|
|
40
|
+
z.boolean(),
|
|
41
|
+
z.strictObject({ value: z.boolean(), when: WhenSchema })
|
|
42
|
+
]);
|
|
43
|
+
var ENTRY_KEY_RE = /^(?:secret|runtime|history|knowledge|settings)\/(?!\/)\S(?:.*\S)?$/;
|
|
44
|
+
var EntriesSchema = z.record(z.string().regex(ENTRY_KEY_RE), EntryValueSchema);
|
|
45
|
+
var LaunchSchema = z.strictObject({
|
|
46
|
+
skipPermissions: z.boolean().optional(),
|
|
47
|
+
remoteControl: z.boolean().optional(),
|
|
48
|
+
provider: z.string().min(1).optional(),
|
|
49
|
+
headroom: z.boolean().optional()
|
|
50
|
+
});
|
|
51
|
+
var CREDENTIAL_TARGETS = ["bearer", "apiKey", "oauthToken"];
|
|
52
|
+
var PROVIDER_CREDENTIAL_TARGETS = ["bearer", "apiKey"];
|
|
53
|
+
var CREDENTIAL_TARGET_VARS = {
|
|
54
|
+
bearer: "ANTHROPIC_AUTH_TOKEN",
|
|
55
|
+
apiKey: "ANTHROPIC_API_KEY",
|
|
56
|
+
oauthToken: "CLAUDE_CODE_OAUTH_TOKEN"
|
|
57
|
+
};
|
|
58
|
+
var ArgvSchema = z.tuple([z.string().min(1)], z.string());
|
|
59
|
+
var CommandSourceOptions = {
|
|
60
|
+
/** True when running the command needs a person present (a desktop-unlocked secret store's approval prompt), so it is skipped when there is neither a terminal nor a desktop session. Defaults per source kind; see `isInteractiveSource`. */
|
|
61
|
+
interactive: z.boolean().optional(),
|
|
62
|
+
/** How long the command may run before it is killed and counted as failed. Defaults to `CREDENTIAL_COMMAND_TIMEOUT_MS`, or `CREDENTIAL_INTERACTIVE_TIMEOUT_MS` for an interactive source. */
|
|
63
|
+
timeoutMs: z.int().positive().optional()
|
|
64
|
+
};
|
|
65
|
+
var CredentialSourceSchema = z.union([
|
|
66
|
+
z.strictObject({ env: z.string().min(1) }),
|
|
67
|
+
z.strictObject({
|
|
68
|
+
file: z.string().min(1).refine((filePath) => filePath.startsWith("~/") || path.isAbsolute(filePath), {
|
|
69
|
+
message: "a credential file must be an absolute or ~-rooted path, since a launch can start in any directory"
|
|
70
|
+
})
|
|
71
|
+
}),
|
|
72
|
+
z.strictObject({ command: ArgvSchema, ...CommandSourceOptions }),
|
|
73
|
+
z.strictObject({ op: z.string().startsWith("op://"), ...CommandSourceOptions }),
|
|
74
|
+
z.strictObject({
|
|
75
|
+
keychain: z.strictObject({ service: z.string().min(1), account: z.string().min(1).optional() }),
|
|
76
|
+
...CommandSourceOptions
|
|
77
|
+
}),
|
|
78
|
+
z.strictObject({ literal: z.string().min(1) })
|
|
79
|
+
]);
|
|
80
|
+
var CREDENTIAL_CACHE_STORES = ["keychain", "file"];
|
|
81
|
+
var CredentialCacheSchema = z.strictObject({
|
|
82
|
+
ttl: z.string().regex(/^[1-9]\d*[smhd]$/, { message: "ttl is a whole number followed by s, m, h or d, for example 12h" }).optional(),
|
|
83
|
+
store: z.enum(CREDENTIAL_CACHE_STORES).optional()
|
|
84
|
+
});
|
|
85
|
+
var CredentialSchema = z.strictObject({
|
|
86
|
+
sources: z.array(CredentialSourceSchema).min(1),
|
|
87
|
+
target: z.enum(CREDENTIAL_TARGETS).optional(),
|
|
88
|
+
cache: CredentialCacheSchema.optional()
|
|
89
|
+
});
|
|
90
|
+
var ProviderCredentialSchema = CredentialSchema.extend({ target: z.enum(PROVIDER_CREDENTIAL_TARGETS).optional() });
|
|
91
|
+
var RESERVED_PROVIDER_ENV_KEYS = Object.values(CREDENTIAL_TARGET_VARS);
|
|
92
|
+
var ProviderEnvSchema = z.record(z.string().min(1), z.string()).refine((env) => !Object.keys(env).some((key) => RESERVED_PROVIDER_ENV_KEYS.includes(key)), {
|
|
93
|
+
message: `env may not set ${RESERVED_PROVIDER_ENV_KEYS.join(", ")}: the credential block's target sets one and claude-use removes the others`
|
|
94
|
+
});
|
|
95
|
+
var CODEX_EFFORTS = ["none", "low", "medium", "high"];
|
|
96
|
+
var CODEX_DEFAULT_TIER_MODELS = Object.freeze({
|
|
97
|
+
fable: "gpt-5.6-sol",
|
|
98
|
+
opus: "gpt-5.6-sol",
|
|
99
|
+
sonnet: "gpt-5.6-terra",
|
|
100
|
+
haiku: "gpt-5.6-luna"
|
|
101
|
+
});
|
|
102
|
+
var CodexProviderConfigSchema = z.strictObject({
|
|
103
|
+
/** The codex model a request maps to when no tier matches its model name. */
|
|
104
|
+
defaultModel: z.string().min(1).optional(),
|
|
105
|
+
/** Per-tier overrides of the shipped mapping, matched by substring of the requested model name in `CODEX_TIERS` order. */
|
|
106
|
+
models: z.strictObject({
|
|
107
|
+
fable: z.string().min(1).optional(),
|
|
108
|
+
opus: z.string().min(1).optional(),
|
|
109
|
+
sonnet: z.string().min(1).optional(),
|
|
110
|
+
haiku: z.string().min(1).optional()
|
|
111
|
+
}).optional(),
|
|
112
|
+
/** The reasoning effort used when the request's own `output_config.effort` is not one the backend accepts. */
|
|
113
|
+
effort: z.enum(CODEX_EFFORTS).optional()
|
|
114
|
+
});
|
|
115
|
+
var HttpProviderSchema = z.strictObject({
|
|
116
|
+
$schema: z.string().optional(),
|
|
117
|
+
kind: z.literal("http").optional(),
|
|
118
|
+
displayName: z.string().min(1),
|
|
119
|
+
baseUrl: z.url(),
|
|
120
|
+
credential: ProviderCredentialSchema,
|
|
121
|
+
env: ProviderEnvSchema.optional()
|
|
122
|
+
});
|
|
123
|
+
var CodexProviderSchema = z.strictObject({
|
|
124
|
+
$schema: z.string().optional(),
|
|
125
|
+
kind: z.literal("codex"),
|
|
126
|
+
displayName: z.string().min(1),
|
|
127
|
+
credential: ProviderCredentialSchema,
|
|
128
|
+
env: ProviderEnvSchema.optional(),
|
|
129
|
+
codex: CodexProviderConfigSchema.optional()
|
|
130
|
+
});
|
|
131
|
+
var ProviderSchema = z.discriminatedUnion("kind", [HttpProviderSchema, CodexProviderSchema]);
|
|
132
|
+
function isCodexProvider(provider) {
|
|
133
|
+
return provider.kind === "codex";
|
|
134
|
+
}
|
|
135
|
+
var ConfigProfileSchema = z.strictObject({
|
|
136
|
+
$schema: z.string().optional(),
|
|
137
|
+
description: z.string().optional(),
|
|
138
|
+
extends: z.array(z.string().min(1)).optional(),
|
|
139
|
+
categories: CategoryMapSchema.optional(),
|
|
140
|
+
entries: EntriesSchema.optional(),
|
|
141
|
+
launch: LaunchSchema.optional()
|
|
142
|
+
});
|
|
143
|
+
var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
|
|
144
|
+
path: z.string().min(1),
|
|
145
|
+
configProfile: z.string().min(1).optional(),
|
|
146
|
+
identity: z.string().min(1).optional(),
|
|
147
|
+
when: WhenSchema.optional()
|
|
148
|
+
});
|
|
149
|
+
var DirectoryRulesSchema = z.strictObject({
|
|
150
|
+
$schema: z.string().optional(),
|
|
151
|
+
rules: z.array(DirectoryRuleSchema)
|
|
152
|
+
});
|
|
153
|
+
var PortableConfigSchema = ConfigProfileSchema.omit({ description: true }).extend({
|
|
154
|
+
configProfile: z.string().min(1).optional(),
|
|
155
|
+
identity: z.string().min(1).optional(),
|
|
156
|
+
when: WhenSchema.optional()
|
|
157
|
+
});
|
|
158
|
+
var HeadroomGlobalConfigSchema = z.strictObject({
|
|
159
|
+
/** Install spec handed to `uv tool install`. Defaults to HEADROOM_DEFAULT_SOURCE. */
|
|
160
|
+
source: z.string().min(1).optional(),
|
|
161
|
+
/** How long the daemon may sit with no registered sessions before the supervisor stops it. Defaults to HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES. */
|
|
162
|
+
idleShutdownMinutes: z.number().int().positive().optional()
|
|
163
|
+
});
|
|
164
|
+
var HEADROOM_DEFAULT_SOURCE = "headroom-ai[proxy] @ git+https://github.com/ExaDev/headroom@eb02fa4126450c9905a49394fdec19ae2e7c9c30";
|
|
165
|
+
var HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES = 15;
|
|
166
|
+
var FrontDoorGlobalConfigSchema = z.strictObject({
|
|
167
|
+
/** How long the front door may sit with no registered sessions before it closes and exits. Defaults to FRONTDOOR_DEFAULT_IDLE_SHUTDOWN_MINUTES. */
|
|
168
|
+
idleShutdownMinutes: z.number().int().positive().optional()
|
|
169
|
+
});
|
|
170
|
+
var GlobalConfigSchema = z.strictObject({
|
|
171
|
+
$schema: z.string().optional(),
|
|
172
|
+
defaultConfigProfile: z.string().min(1).optional(),
|
|
173
|
+
walkUpLimit: z.string().min(1).optional(),
|
|
174
|
+
categories: CategoryMapSchema.optional(),
|
|
175
|
+
entries: EntriesSchema.optional(),
|
|
176
|
+
launch: LaunchSchema.optional(),
|
|
177
|
+
headroom: HeadroomGlobalConfigSchema.optional(),
|
|
178
|
+
frontdoor: FrontDoorGlobalConfigSchema.optional()
|
|
179
|
+
});
|
|
180
|
+
var IdentitySchema = z.strictObject({
|
|
181
|
+
$schema: z.string().optional(),
|
|
182
|
+
name: z.string().min(1).regex(/^[A-Za-z0-9][A-Za-z0-9._@-]*$/),
|
|
183
|
+
defaultConfigProfile: z.string().min(1).optional(),
|
|
184
|
+
allowAmbientCredential: z.boolean().default(false),
|
|
185
|
+
credential: CredentialSchema.optional()
|
|
186
|
+
});
|
|
187
|
+
var CategoryClassificationSchema = z.strictObject({
|
|
188
|
+
$schema: z.string().optional(),
|
|
189
|
+
secret: z.array(z.string().min(1)),
|
|
190
|
+
runtime: z.array(z.string().min(1)),
|
|
191
|
+
history: z.array(z.string().min(1)),
|
|
192
|
+
knowledge: z.array(z.string().min(1)),
|
|
193
|
+
settings: z.array(z.string().min(1))
|
|
194
|
+
});
|
|
195
|
+
var CategoryClassificationOverlaySchema = z.strictObject({
|
|
196
|
+
$schema: z.string().optional(),
|
|
197
|
+
secret: z.array(z.string().min(1)).optional(),
|
|
198
|
+
runtime: z.array(z.string().min(1)).optional(),
|
|
199
|
+
history: z.array(z.string().min(1)).optional(),
|
|
200
|
+
knowledge: z.array(z.string().min(1)).optional(),
|
|
201
|
+
settings: z.array(z.string().min(1)).optional()
|
|
202
|
+
});
|
|
203
|
+
var SHIPPED_CATEGORY_DEFAULTS = Object.freeze({
|
|
204
|
+
secret: false,
|
|
205
|
+
runtime: false,
|
|
206
|
+
history: true,
|
|
207
|
+
knowledge: true,
|
|
208
|
+
settings: true
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// src/config/load.ts
|
|
212
|
+
import { cosmiconfigSync } from "cosmiconfig";
|
|
213
|
+
|
|
214
|
+
// src/cliError.ts
|
|
215
|
+
var EXIT_FAILURE = 1;
|
|
216
|
+
var CliError = class extends Error {
|
|
217
|
+
/** The process exit status this failure maps to. `EXIT_FAILURE` unless a subclass says otherwise. */
|
|
218
|
+
exitCode = EXIT_FAILURE;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// src/config/classify.ts
|
|
222
|
+
var GLOB_METACHARACTERS = /[*?[\]{}!()]/;
|
|
223
|
+
function isExactPattern(pattern) {
|
|
224
|
+
return !GLOB_METACHARACTERS.test(pattern);
|
|
225
|
+
}
|
|
226
|
+
function compile(pattern, category, source, ordinal) {
|
|
227
|
+
const isExact = isExactPattern(pattern);
|
|
228
|
+
const matcher = picomatch(pattern, { dot: true, nocase: false });
|
|
229
|
+
return {
|
|
230
|
+
pattern,
|
|
231
|
+
category,
|
|
232
|
+
source,
|
|
233
|
+
isExact,
|
|
234
|
+
ordinal,
|
|
235
|
+
matches: isExact ? (name) => name === pattern : (name) => matcher(name)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function compileClassificationPatterns(defaults, overlay) {
|
|
239
|
+
const compiled = [];
|
|
240
|
+
let ordinal = 0;
|
|
241
|
+
for (const category of CATEGORY_NAMES) {
|
|
242
|
+
for (const pattern of defaults[category]) {
|
|
243
|
+
compiled.push(compile(pattern, category, "default", ordinal));
|
|
244
|
+
ordinal += 1;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (overlay !== void 0) {
|
|
248
|
+
for (const category of CATEGORY_NAMES) {
|
|
249
|
+
for (const pattern of overlay[category] ?? []) {
|
|
250
|
+
compiled.push(compile(pattern, category, "local", ordinal));
|
|
251
|
+
ordinal += 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return compiled;
|
|
256
|
+
}
|
|
257
|
+
function compareClassificationPatterns(a, b) {
|
|
258
|
+
if (a.isExact !== b.isExact) {
|
|
259
|
+
return a.isExact ? 1 : -1;
|
|
260
|
+
}
|
|
261
|
+
if (a.source !== b.source) {
|
|
262
|
+
return a.source === "local" ? 1 : -1;
|
|
263
|
+
}
|
|
264
|
+
if (a.pattern.length !== b.pattern.length) {
|
|
265
|
+
return a.pattern.length - b.pattern.length;
|
|
266
|
+
}
|
|
267
|
+
return a.ordinal - b.ordinal;
|
|
268
|
+
}
|
|
269
|
+
function classifyEntries(names, options) {
|
|
270
|
+
const patterns = compileClassificationPatterns(options.defaults, options.overlay);
|
|
271
|
+
const classification = /* @__PURE__ */ new Map();
|
|
272
|
+
const decidedBy = /* @__PURE__ */ new Map();
|
|
273
|
+
const unclassified = [];
|
|
274
|
+
for (const name of names) {
|
|
275
|
+
let winner;
|
|
276
|
+
for (const candidate of patterns) {
|
|
277
|
+
if (!candidate.matches(name)) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (winner === void 0 || compareClassificationPatterns(candidate, winner) > 0) {
|
|
281
|
+
winner = candidate;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (winner === void 0) {
|
|
285
|
+
classification.set(name, null);
|
|
286
|
+
unclassified.push(name);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
classification.set(name, winner.category);
|
|
290
|
+
decidedBy.set(name, winner);
|
|
291
|
+
}
|
|
292
|
+
return { classification, unclassified, decidedBy };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/resolve/conditions.ts
|
|
296
|
+
import picomatch2 from "picomatch";
|
|
297
|
+
var MILLISECONDS_PER_UNIT = Object.freeze({
|
|
298
|
+
ms: 1,
|
|
299
|
+
s: 1e3,
|
|
300
|
+
m: 6e4,
|
|
301
|
+
h: 36e5,
|
|
302
|
+
d: 864e5,
|
|
303
|
+
w: 6048e5
|
|
304
|
+
});
|
|
305
|
+
var DURATION_PARTS_RE = /^(0|[1-9][0-9]*)(ms|s|m|h|d|w)$/;
|
|
306
|
+
function parseDuration(value) {
|
|
307
|
+
const parts = DURATION_PARTS_RE.exec(value);
|
|
308
|
+
if (parts === null) {
|
|
309
|
+
throw new Error(`"${value}" is not a valid duration. Expected a count followed by ms, s, m, h, d, or w.`);
|
|
310
|
+
}
|
|
311
|
+
const [, count, unit] = parts;
|
|
312
|
+
if (count === void 0 || unit === void 0) {
|
|
313
|
+
throw new Error(`"${value}" is not a valid duration.`);
|
|
314
|
+
}
|
|
315
|
+
const multiplier = MILLISECONDS_PER_UNIT[unit];
|
|
316
|
+
if (multiplier === void 0) {
|
|
317
|
+
throw new Error(`"${value}" is not a valid duration.`);
|
|
318
|
+
}
|
|
319
|
+
return Number(count) * multiplier;
|
|
320
|
+
}
|
|
321
|
+
function matchBranch(pattern, branch, detached = false) {
|
|
322
|
+
if (branch === void 0 || branch === "" || detached) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
return picomatch2(pattern, { dot: true, nocase: false })(branch);
|
|
326
|
+
}
|
|
327
|
+
function evaluateWhen(when, context) {
|
|
328
|
+
if (when === void 0) {
|
|
329
|
+
return { passed: true, checked: [], failed: [] };
|
|
330
|
+
}
|
|
331
|
+
const checked = [];
|
|
332
|
+
const failed = [];
|
|
333
|
+
if (when.newerThan !== void 0) {
|
|
334
|
+
checked.push("newerThan");
|
|
335
|
+
const window = parseDuration(when.newerThan);
|
|
336
|
+
const latest = context.fact?.latestMtimeMs;
|
|
337
|
+
if (latest === void 0 || context.nowMs - latest > window) {
|
|
338
|
+
failed.push("newerThan");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (when.olderThan !== void 0) {
|
|
342
|
+
checked.push("olderThan");
|
|
343
|
+
const window = parseDuration(when.olderThan);
|
|
344
|
+
const latest = context.fact?.latestMtimeMs;
|
|
345
|
+
if (latest === void 0 || context.nowMs - latest <= window) {
|
|
346
|
+
failed.push("olderThan");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (when.maxSizeBytes !== void 0) {
|
|
350
|
+
checked.push("maxSizeBytes");
|
|
351
|
+
const total = context.fact?.totalSizeBytes;
|
|
352
|
+
if (total === void 0 || total > when.maxSizeBytes) {
|
|
353
|
+
failed.push("maxSizeBytes");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (when.branch !== void 0) {
|
|
357
|
+
checked.push("branch");
|
|
358
|
+
if (!matchBranch(when.branch, context.branch, context.branchDetached ?? false)) {
|
|
359
|
+
failed.push("branch");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (when.env !== void 0) {
|
|
363
|
+
checked.push("env");
|
|
364
|
+
const mismatch = Object.entries(when.env).some(([name, expected]) => context.env[name] !== expected);
|
|
365
|
+
if (mismatch) {
|
|
366
|
+
failed.push("env");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return { passed: failed.length === 0, checked, failed };
|
|
370
|
+
}
|
|
371
|
+
function isVacuousWhen(when) {
|
|
372
|
+
return when !== void 0 && Object.keys(when).length === 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/resolve/match.ts
|
|
376
|
+
import picomatch3 from "picomatch";
|
|
377
|
+
|
|
378
|
+
// src/resolve/projects.ts
|
|
379
|
+
var NON_ALPHANUMERIC = /[^A-Za-z0-9]/g;
|
|
380
|
+
var WILDCARD_TOKENS = ["**", "*", "?"];
|
|
381
|
+
function encodeProjectPath(realPath) {
|
|
382
|
+
return realPath.replace(NON_ALPHANUMERIC, "-");
|
|
383
|
+
}
|
|
384
|
+
function splitOnWildcards(pattern) {
|
|
385
|
+
const fragments = [];
|
|
386
|
+
let literal = "";
|
|
387
|
+
let index = 0;
|
|
388
|
+
while (index < pattern.length) {
|
|
389
|
+
const token = WILDCARD_TOKENS.find((candidate) => pattern.startsWith(candidate, index));
|
|
390
|
+
if (token === void 0) {
|
|
391
|
+
literal += pattern.charAt(index);
|
|
392
|
+
index += 1;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (literal !== "") {
|
|
396
|
+
fragments.push({ kind: "literal", text: literal });
|
|
397
|
+
literal = "";
|
|
398
|
+
}
|
|
399
|
+
fragments.push({ kind: "wildcard", text: token });
|
|
400
|
+
index += token.length;
|
|
401
|
+
}
|
|
402
|
+
if (literal !== "") {
|
|
403
|
+
fragments.push({ kind: "literal", text: literal });
|
|
404
|
+
}
|
|
405
|
+
return fragments;
|
|
406
|
+
}
|
|
407
|
+
var UnrootedProjectPathError = class extends CliError {
|
|
408
|
+
constructor(fragment) {
|
|
409
|
+
super(
|
|
410
|
+
`"${fragment}" is not a rooted path. Everything written after the "history/projects/" prefix is a real absolute working directory (optionally globbed), so it must start with "/" or "~/" \u2014 a bare relative path has nothing to encode against.`
|
|
411
|
+
);
|
|
412
|
+
this.fragment = fragment;
|
|
413
|
+
this.name = "UnrootedProjectPathError";
|
|
414
|
+
}
|
|
415
|
+
fragment;
|
|
416
|
+
};
|
|
417
|
+
function expandHome(fragment, home) {
|
|
418
|
+
if (fragment === "~") {
|
|
419
|
+
return home;
|
|
420
|
+
}
|
|
421
|
+
if (fragment.startsWith("~/")) {
|
|
422
|
+
return home + fragment.slice(1);
|
|
423
|
+
}
|
|
424
|
+
return fragment;
|
|
425
|
+
}
|
|
426
|
+
function encodeProjectPattern(fragment, options) {
|
|
427
|
+
const expanded = expandHome(fragment, options.home);
|
|
428
|
+
if (!expanded.startsWith("/")) {
|
|
429
|
+
throw new UnrootedProjectPathError(fragment);
|
|
430
|
+
}
|
|
431
|
+
return splitOnWildcards(expanded).map((piece) => piece.kind === "literal" ? encodeProjectPath(piece.text) : piece.text).join("");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/resolve/match.ts
|
|
435
|
+
var GLOB_METACHARACTERS2 = /[*?[\]{}!()]/;
|
|
436
|
+
function isExactPattern2(pattern) {
|
|
437
|
+
return !GLOB_METACHARACTERS2.test(pattern);
|
|
438
|
+
}
|
|
439
|
+
function literalPrefixOf(pattern) {
|
|
440
|
+
const match = GLOB_METACHARACTERS2.exec(pattern);
|
|
441
|
+
return match === null ? pattern : pattern.slice(0, match.index);
|
|
442
|
+
}
|
|
443
|
+
function normaliseRelative(fragment) {
|
|
444
|
+
const segments = fragment.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
445
|
+
return segments.join("/");
|
|
446
|
+
}
|
|
447
|
+
var PROJECTS_PREFIX = "projects/";
|
|
448
|
+
var EntryKeyError = class extends CliError {
|
|
449
|
+
constructor(key, message, reason) {
|
|
450
|
+
super(message);
|
|
451
|
+
this.key = key;
|
|
452
|
+
this.reason = reason;
|
|
453
|
+
this.name = "EntryKeyError";
|
|
454
|
+
}
|
|
455
|
+
key;
|
|
456
|
+
reason;
|
|
457
|
+
};
|
|
458
|
+
function canonicaliseEntryKey(key, options) {
|
|
459
|
+
const separator = key.indexOf("/");
|
|
460
|
+
if (separator <= 0) {
|
|
461
|
+
throw new EntryKeyError(key, `Entry key "${key}" has no "<category>/" prefix.`, "malformed");
|
|
462
|
+
}
|
|
463
|
+
const category = key.slice(0, separator);
|
|
464
|
+
if (!isCategoryName(category)) {
|
|
465
|
+
throw new EntryKeyError(key, `Entry key "${key}" starts with "${category}", which is not a category name.`, "malformed");
|
|
466
|
+
}
|
|
467
|
+
const rest = key.slice(separator + 1);
|
|
468
|
+
if (rest === "") {
|
|
469
|
+
throw new EntryKeyError(key, `Entry key "${key}" names a category with no path after it.`, "malformed");
|
|
470
|
+
}
|
|
471
|
+
if (category === "history" && rest.startsWith(PROJECTS_PREFIX)) {
|
|
472
|
+
const fragment = rest.slice(PROJECTS_PREFIX.length);
|
|
473
|
+
try {
|
|
474
|
+
const encoded = encodeProjectPattern(fragment, { home: options.home });
|
|
475
|
+
return { declaredCategory: category, canonicalPattern: PROJECTS_PREFIX + encoded, projectFragment: fragment };
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error instanceof UnrootedProjectPathError) {
|
|
478
|
+
throw new EntryKeyError(key, error.message, "unrooted-project-path");
|
|
479
|
+
}
|
|
480
|
+
throw error;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return { declaredCategory: category, canonicalPattern: normaliseRelative(rest) };
|
|
484
|
+
}
|
|
485
|
+
function compileMatcher(canonicalPattern) {
|
|
486
|
+
const options = { dot: true, nocase: false };
|
|
487
|
+
const self = picomatch3(canonicalPattern, options);
|
|
488
|
+
const subtree = picomatch3(`${canonicalPattern}/**`, options);
|
|
489
|
+
return (relPath) => self(relPath) || subtree(relPath);
|
|
490
|
+
}
|
|
491
|
+
function patternCouldReachUnder(canonicalPattern, dir) {
|
|
492
|
+
const patternSegments = canonicalPattern.split("/").filter((segment) => segment !== "");
|
|
493
|
+
const dirSegments = dir.split("/").filter((segment) => segment !== "");
|
|
494
|
+
return couldReach(patternSegments, dirSegments);
|
|
495
|
+
}
|
|
496
|
+
function couldReach(patternSegments, dirSegments) {
|
|
497
|
+
if (dirSegments.length === 0) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
if (patternSegments.length === 0) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
const [head, ...restPattern] = patternSegments;
|
|
504
|
+
if (head === "**") {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
const [dirHead, ...restDir] = dirSegments;
|
|
508
|
+
if (head === void 0 || dirHead === void 0) {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
if (!picomatch3(head, { dot: true, nocase: false })(dirHead)) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
return couldReach(restPattern, restDir);
|
|
515
|
+
}
|
|
516
|
+
function compareSpecificity(a, b) {
|
|
517
|
+
if (a.layer !== b.layer) {
|
|
518
|
+
return a.layer - b.layer;
|
|
519
|
+
}
|
|
520
|
+
if (a.isExact !== b.isExact) {
|
|
521
|
+
return a.isExact ? 1 : -1;
|
|
522
|
+
}
|
|
523
|
+
if (a.literalPrefix.length !== b.literalPrefix.length) {
|
|
524
|
+
return a.literalPrefix.length - b.literalPrefix.length;
|
|
525
|
+
}
|
|
526
|
+
if (a.segmentCount !== b.segmentCount) {
|
|
527
|
+
return a.segmentCount - b.segmentCount;
|
|
528
|
+
}
|
|
529
|
+
if (a.ordinal !== b.ordinal) {
|
|
530
|
+
return a.ordinal - b.ordinal;
|
|
531
|
+
}
|
|
532
|
+
if (a.canonicalPattern === b.canonicalPattern) {
|
|
533
|
+
return 0;
|
|
534
|
+
}
|
|
535
|
+
return a.canonicalPattern < b.canonicalPattern ? -1 : 1;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/resolve/flatten.ts
|
|
539
|
+
function unpackEntryValue(value) {
|
|
540
|
+
if (typeof value === "boolean") {
|
|
541
|
+
return { value };
|
|
542
|
+
}
|
|
543
|
+
return { value: value.value, when: value.when };
|
|
544
|
+
}
|
|
545
|
+
function flattenLayers(layers, options) {
|
|
546
|
+
const categories = /* @__PURE__ */ new Map();
|
|
547
|
+
const rules = /* @__PURE__ */ new Map();
|
|
548
|
+
const launch = {};
|
|
549
|
+
const diagnostics = [];
|
|
550
|
+
for (const layer of layers) {
|
|
551
|
+
if (layer.categories !== void 0) {
|
|
552
|
+
for (const [name, value] of Object.entries(layer.categories)) {
|
|
553
|
+
if (!isOverridableCategory(name)) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
categories.set(name, value);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (layer.launch !== void 0) {
|
|
560
|
+
if (layer.launch.skipPermissions !== void 0) {
|
|
561
|
+
launch.skipPermissions = layer.launch.skipPermissions;
|
|
562
|
+
}
|
|
563
|
+
if (layer.launch.remoteControl !== void 0) {
|
|
564
|
+
launch.remoteControl = layer.launch.remoteControl;
|
|
565
|
+
}
|
|
566
|
+
if (layer.launch.provider !== void 0) {
|
|
567
|
+
launch.provider = layer.launch.provider;
|
|
568
|
+
}
|
|
569
|
+
if (layer.launch.headroom !== void 0) {
|
|
570
|
+
launch.headroom = layer.launch.headroom;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (layer.entries === void 0) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const order = orderedKeys(layer);
|
|
577
|
+
for (const [ordinal, rawKey] of order.entries()) {
|
|
578
|
+
const value = layer.entries[rawKey];
|
|
579
|
+
if (value === void 0) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
let canonical;
|
|
583
|
+
try {
|
|
584
|
+
canonical = canonicaliseEntryKey(rawKey, { home: options.home });
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (error instanceof EntryKeyError) {
|
|
587
|
+
diagnostics.push({
|
|
588
|
+
code: error.reason === "unrooted-project-path" ? "UNROOTED_PROJECT_PATH" : "MALFORMED_ENTRY_KEY",
|
|
589
|
+
severity: "error",
|
|
590
|
+
message: error.message,
|
|
591
|
+
subject: rawKey,
|
|
592
|
+
layer: layer.id
|
|
593
|
+
});
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
throw error;
|
|
597
|
+
}
|
|
598
|
+
if (canonical.declaredCategory === "secret") {
|
|
599
|
+
diagnostics.push({
|
|
600
|
+
code: "SECRET_ENTRY_KEY",
|
|
601
|
+
severity: "error",
|
|
602
|
+
message: `Entry key "${rawKey}" targets the secret category, which no configuration layer may override. Credentials and config backups are never shared with any identity, and this key is ignored entirely.`,
|
|
603
|
+
subject: rawKey,
|
|
604
|
+
layer: layer.id
|
|
605
|
+
});
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const unpacked = unpackEntryValue(value);
|
|
609
|
+
if (isVacuousWhen(unpacked.when)) {
|
|
610
|
+
diagnostics.push({
|
|
611
|
+
code: "EMPTY_WHEN",
|
|
612
|
+
severity: "warning",
|
|
613
|
+
message: `Entry key "${rawKey}" carries an empty "when" object, which is vacuously true and has no effect.`,
|
|
614
|
+
subject: rawKey,
|
|
615
|
+
layer: layer.id
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
const pattern = canonical.canonicalPattern;
|
|
619
|
+
const rule = {
|
|
620
|
+
rawKey,
|
|
621
|
+
declaredCategory: canonical.declaredCategory,
|
|
622
|
+
canonicalPattern: pattern,
|
|
623
|
+
value: unpacked.value,
|
|
624
|
+
...unpacked.when === void 0 ? {} : { when: unpacked.when },
|
|
625
|
+
layer: layer.id,
|
|
626
|
+
ordinal,
|
|
627
|
+
isExact: isExactPattern2(pattern),
|
|
628
|
+
literalPrefix: literalPrefixOf(pattern),
|
|
629
|
+
segmentCount: pattern.split("/").filter((segment) => segment !== "").length,
|
|
630
|
+
matches: compileMatcher(pattern)
|
|
631
|
+
};
|
|
632
|
+
rules.set(pattern, rule);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return { categories, rules, launch, diagnostics };
|
|
636
|
+
}
|
|
637
|
+
function orderedKeys(layer) {
|
|
638
|
+
const present = new Set(Object.keys(layer.entries ?? {}));
|
|
639
|
+
const ordered = [];
|
|
640
|
+
for (const key of layer.entryOrder ?? []) {
|
|
641
|
+
if (present.has(key)) {
|
|
642
|
+
ordered.push(key);
|
|
643
|
+
present.delete(key);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
for (const key of present) {
|
|
647
|
+
ordered.push(key);
|
|
648
|
+
}
|
|
649
|
+
return ordered;
|
|
650
|
+
}
|
|
651
|
+
function matchingRules(flattened, relPath) {
|
|
652
|
+
const matches = [];
|
|
653
|
+
for (const rule of flattened.rules.values()) {
|
|
654
|
+
if (rule.matches(relPath)) {
|
|
655
|
+
matches.push(rule);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return matches.sort((a, b) => compareSpecificity(b, a));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/resolve/decide.ts
|
|
662
|
+
function selectRule(relPath, flattened, context) {
|
|
663
|
+
const eliminated = [];
|
|
664
|
+
for (const rule of matchingRules(flattened, relPath)) {
|
|
665
|
+
const evaluation = evaluateWhen(rule.when, {
|
|
666
|
+
nowMs: context.nowMs,
|
|
667
|
+
...context.fact === void 0 ? {} : { fact: context.fact },
|
|
668
|
+
...context.branch === void 0 ? {} : { branch: context.branch },
|
|
669
|
+
...context.branchDetached === void 0 ? {} : { branchDetached: context.branchDetached },
|
|
670
|
+
env: context.env
|
|
671
|
+
});
|
|
672
|
+
if (evaluation.passed) {
|
|
673
|
+
return { rule, eliminated };
|
|
674
|
+
}
|
|
675
|
+
eliminated.push({ rule, failed: evaluation.failed });
|
|
676
|
+
}
|
|
677
|
+
return { eliminated };
|
|
678
|
+
}
|
|
679
|
+
function resolveEntry(relPath, params) {
|
|
680
|
+
const diagnostics = [];
|
|
681
|
+
const head = relPath.split("/")[0] ?? relPath;
|
|
682
|
+
const category = params.classification.get(head) ?? null;
|
|
683
|
+
if (category === "secret") {
|
|
684
|
+
for (const rule of matchingRules(params.flattened, relPath)) {
|
|
685
|
+
diagnostics.push({
|
|
686
|
+
code: "SECRET_PATH_NEUTRALISED",
|
|
687
|
+
severity: "warning",
|
|
688
|
+
message: `Entry key "${rule.rawKey}" matches "${relPath}", whose real category is secret. Credentials and config backups are never shared with any identity, so this override is ignored for that path.`,
|
|
689
|
+
subject: relPath,
|
|
690
|
+
layer: rule.layer
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return { decision: { relPath, shared: false, via: "secret-floor", category }, diagnostics };
|
|
694
|
+
}
|
|
695
|
+
if (category === null) {
|
|
696
|
+
diagnostics.push({
|
|
697
|
+
code: "UNCLASSIFIED_ENTRY",
|
|
698
|
+
severity: "warning",
|
|
699
|
+
message: `"${head}" is not in the category map, so it is excluded. Run \`claude-use configure\` to classify it, which records the answer in the local overlay without touching the shipped default map.`,
|
|
700
|
+
subject: head
|
|
701
|
+
});
|
|
702
|
+
return { decision: { relPath, shared: false, via: "unclassified", category }, diagnostics };
|
|
703
|
+
}
|
|
704
|
+
const fact = params.facts.entries.get(relPath);
|
|
705
|
+
const selection = selectRule(relPath, params.flattened, {
|
|
706
|
+
nowMs: params.facts.nowMs,
|
|
707
|
+
...fact === void 0 ? {} : { fact },
|
|
708
|
+
...params.facts.branch === void 0 ? {} : { branch: params.facts.branch },
|
|
709
|
+
...params.facts.branchDetached === void 0 ? {} : { branchDetached: params.facts.branchDetached },
|
|
710
|
+
env: params.facts.env
|
|
711
|
+
});
|
|
712
|
+
if (selection.rule !== void 0) {
|
|
713
|
+
const winner = selection.rule;
|
|
714
|
+
if (winner.declaredCategory !== category) {
|
|
715
|
+
diagnostics.push({
|
|
716
|
+
code: "CATEGORY_PREFIX_MISMATCH",
|
|
717
|
+
severity: "warning",
|
|
718
|
+
message: `Entry key "${winner.rawKey}" declares the ${winner.declaredCategory} category but matches "${relPath}", which is classified as ${category}.`,
|
|
719
|
+
subject: relPath,
|
|
720
|
+
layer: winner.layer
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
diagnostics.push(...reportOverriddenExactKeys(winner, params, relPath));
|
|
724
|
+
return {
|
|
725
|
+
decision: {
|
|
726
|
+
relPath,
|
|
727
|
+
shared: winner.value,
|
|
728
|
+
via: "entry-rule",
|
|
729
|
+
category,
|
|
730
|
+
rule: winner,
|
|
731
|
+
...selection.eliminated.length === 0 ? {} : { eliminated: selection.eliminated }
|
|
732
|
+
},
|
|
733
|
+
diagnostics
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
if (isOverridableCategory(category)) {
|
|
737
|
+
const override = params.flattened.categories.get(category);
|
|
738
|
+
if (override !== void 0) {
|
|
739
|
+
return {
|
|
740
|
+
decision: {
|
|
741
|
+
relPath,
|
|
742
|
+
shared: override,
|
|
743
|
+
via: "category-override",
|
|
744
|
+
category,
|
|
745
|
+
...selection.eliminated.length === 0 ? {} : { eliminated: selection.eliminated }
|
|
746
|
+
},
|
|
747
|
+
diagnostics
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
decision: {
|
|
753
|
+
relPath,
|
|
754
|
+
shared: SHIPPED_CATEGORY_DEFAULTS[category],
|
|
755
|
+
via: "category-default",
|
|
756
|
+
category,
|
|
757
|
+
...selection.eliminated.length === 0 ? {} : { eliminated: selection.eliminated }
|
|
758
|
+
},
|
|
759
|
+
diagnostics
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
function reportOverriddenExactKeys(winner, params, relPath) {
|
|
763
|
+
if (winner.isExact) {
|
|
764
|
+
return [];
|
|
765
|
+
}
|
|
766
|
+
const diagnostics = [];
|
|
767
|
+
for (const candidate of params.flattened.rules.values()) {
|
|
768
|
+
if (candidate === winner || !candidate.isExact || candidate.layer >= winner.layer) {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (!candidate.matches(relPath) || candidate.value === winner.value) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
diagnostics.push({
|
|
775
|
+
code: "EXACT_ENTRY_OVERRIDDEN_BY_LATER_GLOB",
|
|
776
|
+
severity: "info",
|
|
777
|
+
message: `For "${relPath}", the exact key "${candidate.rawKey}" from layer ${String(candidate.layer)} is overridden by the glob "${winner.rawKey}" from the later layer ${String(winner.layer)}. A later layer always wins, so a local rule can only ever tighten what an earlier, shared configuration opened.`,
|
|
778
|
+
subject: relPath,
|
|
779
|
+
layer: winner.layer
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
return diagnostics;
|
|
783
|
+
}
|
|
784
|
+
function resolveAll(params) {
|
|
785
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
786
|
+
const diagnostics = [...params.flattened.diagnostics];
|
|
787
|
+
for (const relPath of params.facts.entries.keys()) {
|
|
788
|
+
const result = resolveEntry(relPath, params);
|
|
789
|
+
decisions.set(relPath, result.decision);
|
|
790
|
+
diagnostics.push(...result.diagnostics);
|
|
791
|
+
}
|
|
792
|
+
return { decisions, diagnostics: dedupeDiagnostics(diagnostics) };
|
|
793
|
+
}
|
|
794
|
+
function dedupeDiagnostics(diagnostics) {
|
|
795
|
+
const seen = /* @__PURE__ */ new Set();
|
|
796
|
+
const unique = [];
|
|
797
|
+
for (const diagnostic of diagnostics) {
|
|
798
|
+
const key = `${diagnostic.code} ${diagnostic.subject ?? ""} ${diagnostic.layer === void 0 ? "" : String(diagnostic.layer)} ${diagnostic.message}`;
|
|
799
|
+
if (seen.has(key)) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
seen.add(key);
|
|
803
|
+
unique.push(diagnostic);
|
|
804
|
+
}
|
|
805
|
+
return unique;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/resolve/plan.ts
|
|
809
|
+
import path2 from "node:path";
|
|
810
|
+
function parentOf(rel) {
|
|
811
|
+
const index = rel.lastIndexOf("/");
|
|
812
|
+
return index === -1 ? "" : rel.slice(0, index);
|
|
813
|
+
}
|
|
814
|
+
function buildChildIndex(facts) {
|
|
815
|
+
const children = /* @__PURE__ */ new Map();
|
|
816
|
+
for (const rel of facts.entries.keys()) {
|
|
817
|
+
const parent = parentOf(rel);
|
|
818
|
+
const bucket = children.get(parent);
|
|
819
|
+
if (bucket === void 0) {
|
|
820
|
+
children.set(parent, [rel]);
|
|
821
|
+
} else {
|
|
822
|
+
bucket.push(rel);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
for (const bucket of children.values()) {
|
|
826
|
+
bucket.sort();
|
|
827
|
+
}
|
|
828
|
+
return children;
|
|
829
|
+
}
|
|
830
|
+
function planFarm(params) {
|
|
831
|
+
const children = buildChildIndex(params.facts);
|
|
832
|
+
const entries = [];
|
|
833
|
+
const materialised = [];
|
|
834
|
+
const links = [];
|
|
835
|
+
const conditionalPatterns = [...params.flattened.rules.values()].filter((rule) => rule.when !== void 0).map((rule) => rule.canonicalPattern);
|
|
836
|
+
const conditionalRuleCouldReach = (dir) => conditionalPatterns.some((pattern) => patternCouldReachUnder(pattern, dir));
|
|
837
|
+
const subtreeDecision = (rel) => {
|
|
838
|
+
const own = params.decisions.get(rel);
|
|
839
|
+
let value = own?.shared;
|
|
840
|
+
const stack = [...children.get(rel) ?? []];
|
|
841
|
+
while (stack.length > 0) {
|
|
842
|
+
const next = stack.pop();
|
|
843
|
+
if (next === void 0) {
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const decision = params.decisions.get(next);
|
|
847
|
+
if (decision === void 0) {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (value === void 0) {
|
|
851
|
+
value = decision.shared;
|
|
852
|
+
} else if (decision.shared !== value) {
|
|
853
|
+
return void 0;
|
|
854
|
+
}
|
|
855
|
+
stack.push(...children.get(next) ?? []);
|
|
856
|
+
}
|
|
857
|
+
return value;
|
|
858
|
+
};
|
|
859
|
+
const linkTarget = (rel) => path2.join(params.facts.claudeHome, rel);
|
|
860
|
+
const visit = (rel) => {
|
|
861
|
+
const fact = params.facts.entries.get(rel);
|
|
862
|
+
const decision = params.decisions.get(rel);
|
|
863
|
+
if (fact === void 0) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
if (!fact.isDirectory) {
|
|
867
|
+
if (decision?.shared === true) {
|
|
868
|
+
entries.push({ kind: "link", rel, target: linkTarget(rel) });
|
|
869
|
+
links.push({ rel, target: linkTarget(rel) });
|
|
870
|
+
} else {
|
|
871
|
+
entries.push({ kind: "omit", rel });
|
|
872
|
+
}
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (conditionalRuleCouldReach(rel)) {
|
|
876
|
+
entries.push({ kind: "materialise", rel, reason: "conditional-rule" });
|
|
877
|
+
materialised.push(rel);
|
|
878
|
+
for (const child of children.get(rel) ?? []) {
|
|
879
|
+
visit(child);
|
|
880
|
+
}
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const uniform = subtreeDecision(rel);
|
|
884
|
+
if (uniform === true) {
|
|
885
|
+
entries.push({ kind: "link", rel, target: linkTarget(rel) });
|
|
886
|
+
links.push({ rel, target: linkTarget(rel) });
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (uniform === false) {
|
|
890
|
+
entries.push({ kind: "omit", rel });
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
entries.push({ kind: "materialise", rel, reason: "split-decision" });
|
|
894
|
+
materialised.push(rel);
|
|
895
|
+
for (const child of children.get(rel) ?? []) {
|
|
896
|
+
visit(child);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
for (const rel of children.get("") ?? []) {
|
|
900
|
+
visit(rel);
|
|
901
|
+
}
|
|
902
|
+
return { entries, materialised, links };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/resolve/extends.ts
|
|
906
|
+
function lineariseProfile(rootName, load) {
|
|
907
|
+
const stack = [];
|
|
908
|
+
const visited = /* @__PURE__ */ new Set();
|
|
909
|
+
const order = [];
|
|
910
|
+
const diagnostics = [];
|
|
911
|
+
const visit = (name) => {
|
|
912
|
+
if (stack.includes(name)) {
|
|
913
|
+
diagnostics.push({
|
|
914
|
+
code: "EXTENDS_CYCLE",
|
|
915
|
+
severity: "error",
|
|
916
|
+
message: `Circular profile extends: ${[...stack, name].join(" -> ")}. The cycle is broken here and "${name}" is not applied twice.`,
|
|
917
|
+
subject: name
|
|
918
|
+
});
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (visited.has(name)) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const source = load(name);
|
|
925
|
+
if (source === void 0) {
|
|
926
|
+
diagnostics.push({
|
|
927
|
+
code: "MISSING_PROFILE",
|
|
928
|
+
severity: "error",
|
|
929
|
+
message: `Configuration profile "${name}" does not exist.`,
|
|
930
|
+
subject: name
|
|
931
|
+
});
|
|
932
|
+
visited.add(name);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
stack.push(name);
|
|
936
|
+
for (const dependency of source.profile.extends ?? []) {
|
|
937
|
+
visit(dependency);
|
|
938
|
+
}
|
|
939
|
+
stack.pop();
|
|
940
|
+
visited.add(name);
|
|
941
|
+
order.push(name);
|
|
942
|
+
};
|
|
943
|
+
visit(rootName);
|
|
944
|
+
return { order, diagnostics };
|
|
945
|
+
}
|
|
946
|
+
function profileLayers(rootName, load, startId) {
|
|
947
|
+
const { order, diagnostics } = lineariseProfile(rootName, load);
|
|
948
|
+
const layers = [];
|
|
949
|
+
let nextId = startId;
|
|
950
|
+
for (const name of order) {
|
|
951
|
+
const source = load(name);
|
|
952
|
+
if (source === void 0) {
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
layers.push({
|
|
956
|
+
id: nextId,
|
|
957
|
+
kind: "config-profile",
|
|
958
|
+
source: source.filepath ?? name,
|
|
959
|
+
...source.profile.categories === void 0 ? {} : { categories: source.profile.categories },
|
|
960
|
+
...source.profile.entries === void 0 ? {} : { entries: source.profile.entries },
|
|
961
|
+
...source.entryOrder === void 0 ? {} : { entryOrder: source.entryOrder },
|
|
962
|
+
...source.profile.launch === void 0 ? {} : { launch: source.profile.launch }
|
|
963
|
+
});
|
|
964
|
+
nextId += 1;
|
|
965
|
+
}
|
|
966
|
+
return { layers, nextId, diagnostics };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/resolve/walk.ts
|
|
970
|
+
function assembleCascade(input) {
|
|
971
|
+
const layers = [];
|
|
972
|
+
const diagnostics = [];
|
|
973
|
+
let nextId = 0;
|
|
974
|
+
let identityPin;
|
|
975
|
+
let directoryConfigProfile;
|
|
976
|
+
if (input.globalConfig !== void 0) {
|
|
977
|
+
const { config, entryOrder, filepath } = input.globalConfig;
|
|
978
|
+
layers.push({
|
|
979
|
+
id: nextId,
|
|
980
|
+
kind: "global-config",
|
|
981
|
+
source: filepath,
|
|
982
|
+
...config.categories === void 0 ? {} : { categories: config.categories },
|
|
983
|
+
...config.entries === void 0 ? {} : { entries: config.entries },
|
|
984
|
+
...entryOrder === void 0 ? {} : { entryOrder },
|
|
985
|
+
...config.launch === void 0 ? {} : { launch: config.launch }
|
|
986
|
+
});
|
|
987
|
+
nextId += 1;
|
|
988
|
+
}
|
|
989
|
+
if (input.baseConfigProfile !== void 0) {
|
|
990
|
+
const resolved = profileLayers(input.baseConfigProfile, input.loadProfile, nextId);
|
|
991
|
+
layers.push(...resolved.layers);
|
|
992
|
+
diagnostics.push(...resolved.diagnostics);
|
|
993
|
+
nextId = resolved.nextId;
|
|
994
|
+
}
|
|
995
|
+
for (const level of input.levels ?? []) {
|
|
996
|
+
const sources = [];
|
|
997
|
+
if (level.portable !== void 0) {
|
|
998
|
+
sources.push({ kind: "portable", config: level.portable.config, ...level.portable.entryOrder === void 0 ? {} : { entryOrder: level.portable.entryOrder }, filepath: level.portable.filepath });
|
|
999
|
+
}
|
|
1000
|
+
for (const entry of level.rules ?? []) {
|
|
1001
|
+
sources.push({ kind: "directory-rule", config: entry.rule, ...entry.entryOrder === void 0 ? {} : { entryOrder: entry.entryOrder }, filepath: entry.filepath });
|
|
1002
|
+
}
|
|
1003
|
+
if (level.portableLocal !== void 0) {
|
|
1004
|
+
sources.push({ kind: "portable-local", config: level.portableLocal.config, ...level.portableLocal.entryOrder === void 0 ? {} : { entryOrder: level.portableLocal.entryOrder }, filepath: level.portableLocal.filepath });
|
|
1005
|
+
}
|
|
1006
|
+
for (const source of sources) {
|
|
1007
|
+
if (source.config.identity !== void 0) {
|
|
1008
|
+
identityPin = source.config.identity;
|
|
1009
|
+
}
|
|
1010
|
+
if (source.config.configProfile !== void 0) {
|
|
1011
|
+
directoryConfigProfile = source.config.configProfile;
|
|
1012
|
+
const resolved = profileLayers(source.config.configProfile, input.loadProfile, nextId);
|
|
1013
|
+
layers.push(...resolved.layers);
|
|
1014
|
+
diagnostics.push(...resolved.diagnostics);
|
|
1015
|
+
nextId = resolved.nextId;
|
|
1016
|
+
}
|
|
1017
|
+
layers.push({
|
|
1018
|
+
id: nextId,
|
|
1019
|
+
kind: source.kind,
|
|
1020
|
+
source: source.filepath,
|
|
1021
|
+
...source.config.categories === void 0 ? {} : { categories: source.config.categories },
|
|
1022
|
+
...source.config.entries === void 0 ? {} : { entries: source.config.entries },
|
|
1023
|
+
...source.entryOrder === void 0 ? {} : { entryOrder: source.entryOrder },
|
|
1024
|
+
...source.config.launch === void 0 ? {} : { launch: source.config.launch }
|
|
1025
|
+
});
|
|
1026
|
+
nextId += 1;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (input.cliOverride !== void 0) {
|
|
1030
|
+
layers.push({
|
|
1031
|
+
id: nextId,
|
|
1032
|
+
kind: "cli-override",
|
|
1033
|
+
source: "command line",
|
|
1034
|
+
...input.cliOverride.categories === void 0 ? {} : { categories: input.cliOverride.categories },
|
|
1035
|
+
...input.cliOverride.entries === void 0 ? {} : { entries: input.cliOverride.entries },
|
|
1036
|
+
...input.cliOverride.entryOrder === void 0 ? {} : { entryOrder: input.cliOverride.entryOrder },
|
|
1037
|
+
...input.cliOverride.launch === void 0 ? {} : { launch: input.cliOverride.launch }
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
layers,
|
|
1042
|
+
diagnostics,
|
|
1043
|
+
...identityPin === void 0 ? {} : { identityPin },
|
|
1044
|
+
...directoryConfigProfile === void 0 ? {} : { directoryConfigProfile }
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/resolve/pipeline.ts
|
|
1049
|
+
function topLevelNames(facts) {
|
|
1050
|
+
const names = /* @__PURE__ */ new Set();
|
|
1051
|
+
for (const rel of facts.entries.keys()) {
|
|
1052
|
+
const head = rel.split("/")[0];
|
|
1053
|
+
if (head !== void 0 && head !== "") {
|
|
1054
|
+
names.add(head);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return [...names].sort();
|
|
1058
|
+
}
|
|
1059
|
+
function resolveDecisions(input) {
|
|
1060
|
+
const assembled = assembleCascade(input.cascade);
|
|
1061
|
+
const flattened = flattenLayers(assembled.layers, { home: input.facts.home });
|
|
1062
|
+
const classification = classifyEntries(topLevelNames(input.facts), {
|
|
1063
|
+
defaults: input.classification.defaults,
|
|
1064
|
+
...input.classification.overlay === void 0 ? {} : { overlay: input.classification.overlay }
|
|
1065
|
+
});
|
|
1066
|
+
const resolved = resolveAll({
|
|
1067
|
+
flattened,
|
|
1068
|
+
facts: input.facts,
|
|
1069
|
+
classification: classification.classification
|
|
1070
|
+
});
|
|
1071
|
+
const farm = planFarm({ facts: input.facts, decisions: resolved.decisions, flattened });
|
|
1072
|
+
return {
|
|
1073
|
+
assembled,
|
|
1074
|
+
flattened,
|
|
1075
|
+
classification,
|
|
1076
|
+
decisions: resolved.decisions,
|
|
1077
|
+
farm,
|
|
1078
|
+
diagnostics: [...assembled.diagnostics, ...resolved.diagnostics]
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// src/launcher/farm.ts
|
|
1083
|
+
import path4 from "node:path";
|
|
1084
|
+
import { z as z2 } from "zod";
|
|
1085
|
+
|
|
1086
|
+
// src/resolve/reconcile.ts
|
|
1087
|
+
function parentOf2(rel) {
|
|
1088
|
+
const index = rel.lastIndexOf("/");
|
|
1089
|
+
return index === -1 ? "" : rel.slice(0, index);
|
|
1090
|
+
}
|
|
1091
|
+
function indexByParent(listing) {
|
|
1092
|
+
const children = /* @__PURE__ */ new Map();
|
|
1093
|
+
for (const entry of listing) {
|
|
1094
|
+
const parent = parentOf2(entry.rel);
|
|
1095
|
+
const bucket = children.get(parent);
|
|
1096
|
+
if (bucket === void 0) {
|
|
1097
|
+
children.set(parent, [entry]);
|
|
1098
|
+
} else {
|
|
1099
|
+
bucket.push(entry);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
for (const bucket of children.values()) {
|
|
1103
|
+
bucket.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
1104
|
+
}
|
|
1105
|
+
return children;
|
|
1106
|
+
}
|
|
1107
|
+
function planReconciliation(params) {
|
|
1108
|
+
const conservative = params.manifest === void 0;
|
|
1109
|
+
const diagnostics = [];
|
|
1110
|
+
const actions = [];
|
|
1111
|
+
const farmChildren = indexByParent(params.farmListing);
|
|
1112
|
+
const canonicalByRel = new Map(params.canonicalListing.map((entry) => [entry.rel, entry]));
|
|
1113
|
+
const materialised = /* @__PURE__ */ new Set([""]);
|
|
1114
|
+
if (params.manifest !== void 0) {
|
|
1115
|
+
for (const rel of params.manifest.materialised) {
|
|
1116
|
+
materialised.add(rel);
|
|
1117
|
+
}
|
|
1118
|
+
} else {
|
|
1119
|
+
for (const entry of params.farmListing) {
|
|
1120
|
+
if (entry.kind === "dir") {
|
|
1121
|
+
materialised.add(entry.rel);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
diagnostics.push({
|
|
1125
|
+
code: "FARM_MANIFEST_MISSING",
|
|
1126
|
+
severity: "warning",
|
|
1127
|
+
message: "The previous farm's manifest is missing or unreadable, so there is no record of which directories the farm built itself. Every real directory in the farm was treated as materialised and every real child examined for adoption. This over-adopts rather than risking data Claude Code wrote being discarded."
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
const visit = (dir) => {
|
|
1131
|
+
for (const child of farmChildren.get(dir) ?? []) {
|
|
1132
|
+
if (child.kind === "symlink") {
|
|
1133
|
+
actions.push({ kind: "skip", rel: child.rel, reason: "is-symlink" });
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
if (child.kind === "dir" && materialised.has(child.rel)) {
|
|
1137
|
+
actions.push({ kind: "skip", rel: child.rel, reason: "tracked-materialised-dir" });
|
|
1138
|
+
visit(child.rel);
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
if (child.kind === "dir") {
|
|
1142
|
+
const grandchildren = farmChildren.get(child.rel) ?? [];
|
|
1143
|
+
if (grandchildren.length === 0) {
|
|
1144
|
+
if (canonicalByRel.has(child.rel)) {
|
|
1145
|
+
actions.push({ kind: "skip", rel: child.rel, reason: "identical-content" });
|
|
1146
|
+
} else {
|
|
1147
|
+
actions.push({ kind: "adopt", rel: child.rel, reason: "absent-from-canonical" });
|
|
1148
|
+
}
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
visit(child.rel);
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
const canonical = canonicalByRel.get(child.rel);
|
|
1155
|
+
if (canonical === void 0) {
|
|
1156
|
+
actions.push({ kind: "adopt", rel: child.rel, reason: "absent-from-canonical" });
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
if (canonical.contentHash !== void 0 && canonical.contentHash === child.contentHash) {
|
|
1160
|
+
actions.push({ kind: "skip", rel: child.rel, reason: "identical-content" });
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
actions.push({ kind: "adopt-conflict", rel: child.rel });
|
|
1164
|
+
diagnostics.push({
|
|
1165
|
+
code: "RECONCILE_CONFLICT",
|
|
1166
|
+
severity: "warning",
|
|
1167
|
+
message: `"${child.rel}" exists both in the farm and in the canonical ~/.claude with different content. The canonical copy is left authoritative and the farm's copy is preserved alongside it rather than either being discarded.`,
|
|
1168
|
+
subject: child.rel
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
visit("");
|
|
1173
|
+
return { actions, diagnostics, conservative };
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/launcher/lock.ts
|
|
1177
|
+
import { randomUUID } from "node:crypto";
|
|
1178
|
+
import path3 from "node:path";
|
|
1179
|
+
var DEFAULT_STALE_AFTER_MS = 12e4;
|
|
1180
|
+
var DEFAULT_RETRY_DELAY_MS = 50;
|
|
1181
|
+
var DEFAULT_MAX_ATTEMPTS = 200;
|
|
1182
|
+
var IdentityLockBusyError = class extends CliError {
|
|
1183
|
+
constructor(identity, lockPath, holderPid) {
|
|
1184
|
+
super(
|
|
1185
|
+
`Another claude-use resync is already running for identity "${identity}"` + (holderPid === void 0 ? "" : ` (pid ${String(holderPid)})`) + `. Its lock at ${lockPath} was still held after the full retry budget; nothing was changed.`
|
|
1186
|
+
);
|
|
1187
|
+
this.identity = identity;
|
|
1188
|
+
this.lockPath = lockPath;
|
|
1189
|
+
this.holderPid = holderPid;
|
|
1190
|
+
this.name = "IdentityLockBusyError";
|
|
1191
|
+
}
|
|
1192
|
+
identity;
|
|
1193
|
+
lockPath;
|
|
1194
|
+
holderPid;
|
|
1195
|
+
};
|
|
1196
|
+
function identityLockPath(dir, identity) {
|
|
1197
|
+
return path3.join(dir, `.${identity}.lock`);
|
|
1198
|
+
}
|
|
1199
|
+
function parseLockRecord(raw) {
|
|
1200
|
+
if (raw === void 0) {
|
|
1201
|
+
return void 0;
|
|
1202
|
+
}
|
|
1203
|
+
let parsed;
|
|
1204
|
+
try {
|
|
1205
|
+
parsed = JSON.parse(raw);
|
|
1206
|
+
} catch {
|
|
1207
|
+
return void 0;
|
|
1208
|
+
}
|
|
1209
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1210
|
+
return void 0;
|
|
1211
|
+
}
|
|
1212
|
+
if (!("identity" in parsed) || !("pid" in parsed) || !("token" in parsed) || !("acquiredAtMs" in parsed)) {
|
|
1213
|
+
return void 0;
|
|
1214
|
+
}
|
|
1215
|
+
const { identity, pid, token, acquiredAtMs } = parsed;
|
|
1216
|
+
if (typeof identity !== "string" || typeof pid !== "number" || typeof token !== "string" || typeof acquiredAtMs !== "number") {
|
|
1217
|
+
return void 0;
|
|
1218
|
+
}
|
|
1219
|
+
return { identity, pid, token, acquiredAtMs };
|
|
1220
|
+
}
|
|
1221
|
+
function acquireIdentityLock(params) {
|
|
1222
|
+
const lockPath = identityLockPath(params.dir, params.identity);
|
|
1223
|
+
const staleAfterMs = params.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
1224
|
+
const retryDelayMs = params.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
1225
|
+
const maxAttempts = params.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
1226
|
+
const token = randomUUID();
|
|
1227
|
+
if (params.fs.lstat(params.dir) === void 0) {
|
|
1228
|
+
params.fs.mkdirp(params.dir);
|
|
1229
|
+
}
|
|
1230
|
+
let lastHolderPid;
|
|
1231
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1232
|
+
const record = { identity: params.identity, pid: params.pid, token, acquiredAtMs: params.nowMs() };
|
|
1233
|
+
if (params.fs.writeFileExclusive(lockPath, `${JSON.stringify(record)}
|
|
1234
|
+
`)) {
|
|
1235
|
+
return {
|
|
1236
|
+
path: lockPath,
|
|
1237
|
+
token,
|
|
1238
|
+
release: () => {
|
|
1239
|
+
const current = parseLockRecord(params.fs.readFileUtf8(lockPath));
|
|
1240
|
+
if (current !== void 0 && current.token !== token) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
params.fs.removeRecursive(lockPath);
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
const existing = parseLockRecord(params.fs.readFileUtf8(lockPath));
|
|
1248
|
+
if (existing === void 0) {
|
|
1249
|
+
params.fs.removeRecursive(lockPath);
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
lastHolderPid = existing.pid;
|
|
1253
|
+
const expired = params.nowMs() - existing.acquiredAtMs > staleAfterMs;
|
|
1254
|
+
if (expired || !params.isRunning(existing.pid)) {
|
|
1255
|
+
params.fs.removeRecursive(lockPath);
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
params.sleep(retryDelayMs);
|
|
1259
|
+
}
|
|
1260
|
+
throw new IdentityLockBusyError(params.identity, lockPath, lastHolderPid);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/launcher/farm.ts
|
|
1264
|
+
var FARM_MANIFEST_FILENAME = ".claude-use-farm.json";
|
|
1265
|
+
var FarmManifestSchema = z2.strictObject({
|
|
1266
|
+
version: z2.literal(1),
|
|
1267
|
+
builtAtMs: z2.number(),
|
|
1268
|
+
identity: z2.string(),
|
|
1269
|
+
configProfile: z2.string().optional(),
|
|
1270
|
+
cwd: z2.string(),
|
|
1271
|
+
claudeHome: z2.string(),
|
|
1272
|
+
materialised: z2.array(z2.string()),
|
|
1273
|
+
links: z2.array(z2.strictObject({ rel: z2.string(), target: z2.string() }))
|
|
1274
|
+
});
|
|
1275
|
+
function readFarmManifest(fs2, farmRoot) {
|
|
1276
|
+
const raw = fs2.readFileUtf8(path4.join(farmRoot, FARM_MANIFEST_FILENAME));
|
|
1277
|
+
if (raw === void 0) {
|
|
1278
|
+
return void 0;
|
|
1279
|
+
}
|
|
1280
|
+
let parsed;
|
|
1281
|
+
try {
|
|
1282
|
+
parsed = JSON.parse(raw);
|
|
1283
|
+
} catch {
|
|
1284
|
+
return void 0;
|
|
1285
|
+
}
|
|
1286
|
+
const result = FarmManifestSchema.safeParse(parsed);
|
|
1287
|
+
return result.success ? result.data : void 0;
|
|
1288
|
+
}
|
|
1289
|
+
function buildEntryFacts(params) {
|
|
1290
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1291
|
+
const visit = (rel) => {
|
|
1292
|
+
const absolute = path4.join(params.claudeHome, rel);
|
|
1293
|
+
const stat = params.fs.lstat(absolute);
|
|
1294
|
+
if (stat === void 0) {
|
|
1295
|
+
return void 0;
|
|
1296
|
+
}
|
|
1297
|
+
const isDirectory = stat.kind === "dir";
|
|
1298
|
+
let latestMtimeMs = stat.mtimeMs;
|
|
1299
|
+
let totalSizeBytes = stat.sizeBytes;
|
|
1300
|
+
if (isDirectory) {
|
|
1301
|
+
for (const name of [...params.fs.readdir(absolute)].sort()) {
|
|
1302
|
+
const child = visit(`${rel}/${name}`);
|
|
1303
|
+
if (child !== void 0) {
|
|
1304
|
+
latestMtimeMs = Math.max(latestMtimeMs, child.latestMtimeMs);
|
|
1305
|
+
totalSizeBytes += child.totalSizeBytes;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
entries.set(rel, {
|
|
1310
|
+
relPath: rel,
|
|
1311
|
+
isDirectory,
|
|
1312
|
+
isSymlink: stat.kind === "symlink",
|
|
1313
|
+
mtimeMs: stat.mtimeMs,
|
|
1314
|
+
latestMtimeMs,
|
|
1315
|
+
sizeBytes: stat.sizeBytes,
|
|
1316
|
+
totalSizeBytes
|
|
1317
|
+
});
|
|
1318
|
+
return { latestMtimeMs, totalSizeBytes };
|
|
1319
|
+
};
|
|
1320
|
+
for (const name of [...params.fs.readdir(params.claudeHome)].sort()) {
|
|
1321
|
+
visit(name);
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
nowMs: params.nowMs,
|
|
1325
|
+
home: params.home,
|
|
1326
|
+
claudeHome: params.claudeHome,
|
|
1327
|
+
cwd: params.cwd,
|
|
1328
|
+
...params.branch === void 0 ? {} : { branch: params.branch },
|
|
1329
|
+
...params.branchDetached === void 0 ? {} : { branchDetached: params.branchDetached },
|
|
1330
|
+
env: params.env,
|
|
1331
|
+
entries
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
function listSubtree(fs2, root, rel) {
|
|
1335
|
+
const absolute = path4.join(root, rel);
|
|
1336
|
+
const stat = fs2.lstat(absolute);
|
|
1337
|
+
if (stat === void 0) {
|
|
1338
|
+
return [];
|
|
1339
|
+
}
|
|
1340
|
+
if (stat.kind === "symlink") {
|
|
1341
|
+
return [{ rel, kind: "symlink" }];
|
|
1342
|
+
}
|
|
1343
|
+
if (stat.kind === "dir") {
|
|
1344
|
+
const children = [...fs2.readdir(absolute)].sort().flatMap((name) => listSubtree(fs2, root, `${rel}/${name}`));
|
|
1345
|
+
return [{ rel, kind: "dir" }, ...children];
|
|
1346
|
+
}
|
|
1347
|
+
const contentHash = fs2.hashFile(absolute);
|
|
1348
|
+
return [{ rel, kind: "file", ...contentHash === void 0 ? {} : { contentHash } }];
|
|
1349
|
+
}
|
|
1350
|
+
function topLevelScopeRoots(roots) {
|
|
1351
|
+
const sorted = [...new Set(roots)].sort();
|
|
1352
|
+
const kept = [];
|
|
1353
|
+
for (const root of sorted) {
|
|
1354
|
+
if (kept.some((existing) => root === existing || root.startsWith(`${existing}/`))) {
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
kept.push(root);
|
|
1358
|
+
}
|
|
1359
|
+
return kept;
|
|
1360
|
+
}
|
|
1361
|
+
function reconciliationScope(fs2, farmRoot, manifest) {
|
|
1362
|
+
if (manifest !== void 0) {
|
|
1363
|
+
return topLevelScopeRoots(manifest.materialised);
|
|
1364
|
+
}
|
|
1365
|
+
return [...fs2.readdir(farmRoot)].filter((name) => name !== FARM_MANIFEST_FILENAME && fs2.lstat(path4.join(farmRoot, name))?.kind === "dir").sort();
|
|
1366
|
+
}
|
|
1367
|
+
function collectFarmListing(fs2, farmRoot, scopeRoots) {
|
|
1368
|
+
return scopeRoots.flatMap((root) => listSubtree(fs2, farmRoot, root));
|
|
1369
|
+
}
|
|
1370
|
+
function collectCanonicalCounterparts(fs2, claudeHome, farmListing) {
|
|
1371
|
+
const out = [];
|
|
1372
|
+
for (const entry of farmListing) {
|
|
1373
|
+
const absolute = path4.join(claudeHome, entry.rel);
|
|
1374
|
+
const stat = fs2.lstat(absolute);
|
|
1375
|
+
if (stat === void 0) {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (stat.kind === "file") {
|
|
1379
|
+
const contentHash = fs2.hashFile(absolute);
|
|
1380
|
+
out.push({ rel: entry.rel, kind: "file", ...contentHash === void 0 ? {} : { contentHash } });
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
out.push({ rel: entry.rel, kind: stat.kind });
|
|
1384
|
+
}
|
|
1385
|
+
return out;
|
|
1386
|
+
}
|
|
1387
|
+
function carryOver(params) {
|
|
1388
|
+
const manifest = readFarmManifest(params.fs, params.previousRoot);
|
|
1389
|
+
const accounted = /* @__PURE__ */ new Set([FARM_MANIFEST_FILENAME]);
|
|
1390
|
+
for (const rel of manifest?.materialised ?? []) {
|
|
1391
|
+
const head = rel.split("/")[0];
|
|
1392
|
+
if (head !== void 0 && head !== "") {
|
|
1393
|
+
accounted.add(head);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const carried = [];
|
|
1397
|
+
const autoResolved = [];
|
|
1398
|
+
const collided = [];
|
|
1399
|
+
for (const name of [...params.fs.readdir(params.previousRoot)].sort()) {
|
|
1400
|
+
if (accounted.has(name)) {
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const stat = params.fs.lstat(path4.join(params.previousRoot, name));
|
|
1404
|
+
if (stat === void 0 || stat.kind === "symlink") {
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
if (params.fs.lstat(path4.join(params.farmRoot, name)) !== void 0) {
|
|
1408
|
+
const category = params.classification === void 0 ? void 0 : classifyEntries([name], params.classification).classification.get(name);
|
|
1409
|
+
if (category === "runtime") {
|
|
1410
|
+
params.fs.removeRecursive(path4.join(params.previousRoot, name));
|
|
1411
|
+
autoResolved.push(name);
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
collided.push(name);
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
params.fs.rename(path4.join(params.previousRoot, name), path4.join(params.farmRoot, name));
|
|
1418
|
+
carried.push(name);
|
|
1419
|
+
}
|
|
1420
|
+
return { carried, autoResolved, collided };
|
|
1421
|
+
}
|
|
1422
|
+
function buildScratchTree(params) {
|
|
1423
|
+
params.fs.mkdirp(params.scratchRoot);
|
|
1424
|
+
for (const entry of params.plan.entries) {
|
|
1425
|
+
const absolute = path4.join(params.scratchRoot, entry.rel);
|
|
1426
|
+
if (entry.kind === "link") {
|
|
1427
|
+
params.fs.mkdirp(path4.dirname(absolute));
|
|
1428
|
+
params.fs.symlink(entry.target, absolute);
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
if (entry.kind === "materialise") {
|
|
1432
|
+
params.fs.mkdirp(absolute);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
params.fs.writeFileUtf8(
|
|
1436
|
+
path4.join(params.scratchRoot, FARM_MANIFEST_FILENAME),
|
|
1437
|
+
`${JSON.stringify(params.manifest, null, 2)}
|
|
1438
|
+
`
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
function swapIn(params) {
|
|
1442
|
+
if (params.fs.lstat(params.farmRoot) === void 0) {
|
|
1443
|
+
params.fs.mkdirp(path4.dirname(params.farmRoot));
|
|
1444
|
+
params.fs.rename(params.scratchRoot, params.farmRoot);
|
|
1445
|
+
return { carried: [], autoResolved: [], collided: [] };
|
|
1446
|
+
}
|
|
1447
|
+
params.fs.rename(params.farmRoot, params.previousRoot);
|
|
1448
|
+
params.fs.rename(params.scratchRoot, params.farmRoot);
|
|
1449
|
+
const result = carryOver({
|
|
1450
|
+
fs: params.fs,
|
|
1451
|
+
previousRoot: params.previousRoot,
|
|
1452
|
+
farmRoot: params.farmRoot,
|
|
1453
|
+
...params.classification === void 0 ? {} : { classification: params.classification }
|
|
1454
|
+
});
|
|
1455
|
+
if (result.collided.length === 0) {
|
|
1456
|
+
params.fs.removeRecursive(params.previousRoot);
|
|
1457
|
+
return result;
|
|
1458
|
+
}
|
|
1459
|
+
return { ...result, retainedPrevious: params.previousRoot };
|
|
1460
|
+
}
|
|
1461
|
+
function recoverInterruptedSwap(params) {
|
|
1462
|
+
const scratchPrefix = `.${params.identity}.scratch.`;
|
|
1463
|
+
const previousPrefix = `.${params.identity}.previous.`;
|
|
1464
|
+
const names = [...params.fs.readdir(params.identitiesDir)].sort();
|
|
1465
|
+
const removedScratch = [];
|
|
1466
|
+
for (const name of names.filter((candidate) => candidate.startsWith(scratchPrefix))) {
|
|
1467
|
+
params.fs.removeRecursive(path4.join(params.identitiesDir, name));
|
|
1468
|
+
removedScratch.push(name);
|
|
1469
|
+
}
|
|
1470
|
+
let previous = names.filter((candidate) => candidate.startsWith(previousPrefix));
|
|
1471
|
+
let restoredFrom;
|
|
1472
|
+
const first = previous[0];
|
|
1473
|
+
if (first !== void 0 && params.fs.lstat(params.farmRoot) === void 0) {
|
|
1474
|
+
params.fs.rename(path4.join(params.identitiesDir, first), params.farmRoot);
|
|
1475
|
+
restoredFrom = first;
|
|
1476
|
+
previous = previous.slice(1);
|
|
1477
|
+
}
|
|
1478
|
+
const completed = [];
|
|
1479
|
+
const autoResolved = [];
|
|
1480
|
+
const retained = [];
|
|
1481
|
+
for (const name of previous) {
|
|
1482
|
+
const previousRoot = path4.join(params.identitiesDir, name);
|
|
1483
|
+
const result = carryOver({
|
|
1484
|
+
fs: params.fs,
|
|
1485
|
+
previousRoot,
|
|
1486
|
+
farmRoot: params.farmRoot,
|
|
1487
|
+
...params.classification === void 0 ? {} : { classification: params.classification }
|
|
1488
|
+
});
|
|
1489
|
+
autoResolved.push(...result.autoResolved);
|
|
1490
|
+
if (result.collided.length === 0) {
|
|
1491
|
+
params.fs.removeRecursive(previousRoot);
|
|
1492
|
+
completed.push(name);
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
retained.push(previousRoot);
|
|
1496
|
+
}
|
|
1497
|
+
return {
|
|
1498
|
+
removedScratch,
|
|
1499
|
+
...restoredFrom === void 0 ? {} : { restoredFrom },
|
|
1500
|
+
completed,
|
|
1501
|
+
autoResolved,
|
|
1502
|
+
retained,
|
|
1503
|
+
recovered: removedScratch.length > 0 || restoredFrom !== void 0 || completed.length > 0 || autoResolved.length > 0 || retained.length > 0
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function recoverFarm(params) {
|
|
1507
|
+
const farmRoot = path4.join(params.identitiesDir, params.identity);
|
|
1508
|
+
const lock = acquireIdentityLock({
|
|
1509
|
+
identity: params.identity,
|
|
1510
|
+
dir: params.identitiesDir,
|
|
1511
|
+
fs: params.fs,
|
|
1512
|
+
nowMs: params.now,
|
|
1513
|
+
pid: params.lock.pid,
|
|
1514
|
+
isRunning: params.lock.isRunning,
|
|
1515
|
+
sleep: params.lock.sleep,
|
|
1516
|
+
...params.lock.staleAfterMs === void 0 ? {} : { staleAfterMs: params.lock.staleAfterMs },
|
|
1517
|
+
...params.lock.retryDelayMs === void 0 ? {} : { retryDelayMs: params.lock.retryDelayMs },
|
|
1518
|
+
...params.lock.maxAttempts === void 0 ? {} : { maxAttempts: params.lock.maxAttempts }
|
|
1519
|
+
});
|
|
1520
|
+
try {
|
|
1521
|
+
return recoverInterruptedSwap({
|
|
1522
|
+
fs: params.fs,
|
|
1523
|
+
identitiesDir: params.identitiesDir,
|
|
1524
|
+
identity: params.identity,
|
|
1525
|
+
farmRoot,
|
|
1526
|
+
...params.classification === void 0 ? {} : { classification: params.classification }
|
|
1527
|
+
});
|
|
1528
|
+
} finally {
|
|
1529
|
+
lock.release();
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function executeReconciliation(params) {
|
|
1533
|
+
const heads = /* @__PURE__ */ new Set();
|
|
1534
|
+
for (const action of params.actions) {
|
|
1535
|
+
if (action.kind === "skip") {
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
const head = action.rel.split("/")[0];
|
|
1539
|
+
if (head !== void 0 && head !== "") {
|
|
1540
|
+
heads.add(head);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
const classified = classifyEntries([...heads], {
|
|
1544
|
+
defaults: params.classification.defaults,
|
|
1545
|
+
...params.classification.overlay === void 0 ? {} : { overlay: params.classification.overlay }
|
|
1546
|
+
});
|
|
1547
|
+
const adopted = [];
|
|
1548
|
+
const conflicts = [];
|
|
1549
|
+
const blocked = [];
|
|
1550
|
+
const diagnostics = [];
|
|
1551
|
+
for (const action of params.actions) {
|
|
1552
|
+
if (action.kind === "skip") {
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
const head = action.rel.split("/")[0] ?? "";
|
|
1556
|
+
if (classified.classification.get(head) === "secret") {
|
|
1557
|
+
blocked.push(action.rel);
|
|
1558
|
+
diagnostics.push({
|
|
1559
|
+
code: "RECONCILE_SECRET_BLOCKED",
|
|
1560
|
+
severity: "warning",
|
|
1561
|
+
message: `"${action.rel}" was found in the farm but was not copied into ~/.claude: its top-level entry "${head}" is classified secret, and no configuration layer or reconciliation pass may move data across that floor.`,
|
|
1562
|
+
subject: action.rel
|
|
1563
|
+
});
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
const from = path4.join(params.farmRoot, action.rel);
|
|
1567
|
+
const canonical = path4.join(params.claudeHome, action.rel);
|
|
1568
|
+
if (action.kind === "adopt") {
|
|
1569
|
+
params.fs.mkdirp(path4.dirname(canonical));
|
|
1570
|
+
params.fs.copyRecursive(from, canonical);
|
|
1571
|
+
adopted.push(action.rel);
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
const preserved = `${canonical}.farm-conflict-${String(params.nowMs)}`;
|
|
1575
|
+
params.fs.mkdirp(path4.dirname(canonical));
|
|
1576
|
+
params.fs.copyRecursive(from, preserved);
|
|
1577
|
+
conflicts.push(action.rel);
|
|
1578
|
+
}
|
|
1579
|
+
return { adopted, conflicts, blocked, diagnostics };
|
|
1580
|
+
}
|
|
1581
|
+
function sameLinks(a, b) {
|
|
1582
|
+
if (a.length !== b.length) {
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
return a.every((link, index) => {
|
|
1586
|
+
const other = b[index];
|
|
1587
|
+
if (other === void 0) {
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
return other.rel === link.rel && other.target === link.target;
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
function sameStrings(a, b) {
|
|
1594
|
+
return a.length === b.length && a.every((value, index) => b[index] === value);
|
|
1595
|
+
}
|
|
1596
|
+
function farmAlreadyMatches(existing, next) {
|
|
1597
|
+
return existing.identity === next.identity && existing.claudeHome === next.claudeHome && sameStrings(existing.materialised, next.materialised) && sameLinks(existing.links, next.links);
|
|
1598
|
+
}
|
|
1599
|
+
function resyncFarm(params) {
|
|
1600
|
+
const farmRoot = path4.join(params.identitiesDir, params.identity);
|
|
1601
|
+
const lock = acquireIdentityLock({
|
|
1602
|
+
identity: params.identity,
|
|
1603
|
+
dir: params.identitiesDir,
|
|
1604
|
+
fs: params.fs,
|
|
1605
|
+
nowMs: params.now,
|
|
1606
|
+
pid: params.lock.pid,
|
|
1607
|
+
isRunning: params.lock.isRunning,
|
|
1608
|
+
sleep: params.lock.sleep,
|
|
1609
|
+
...params.lock.staleAfterMs === void 0 ? {} : { staleAfterMs: params.lock.staleAfterMs },
|
|
1610
|
+
...params.lock.retryDelayMs === void 0 ? {} : { retryDelayMs: params.lock.retryDelayMs },
|
|
1611
|
+
...params.lock.maxAttempts === void 0 ? {} : { maxAttempts: params.lock.maxAttempts }
|
|
1612
|
+
});
|
|
1613
|
+
try {
|
|
1614
|
+
const recovery = recoverInterruptedSwap({
|
|
1615
|
+
fs: params.fs,
|
|
1616
|
+
identitiesDir: params.identitiesDir,
|
|
1617
|
+
identity: params.identity,
|
|
1618
|
+
farmRoot,
|
|
1619
|
+
classification: params.classification
|
|
1620
|
+
});
|
|
1621
|
+
const previousManifest = readFarmManifest(params.fs, farmRoot);
|
|
1622
|
+
const scope = reconciliationScope(params.fs, farmRoot, previousManifest);
|
|
1623
|
+
const farmListing = collectFarmListing(params.fs, farmRoot, scope);
|
|
1624
|
+
const canonicalListing = collectCanonicalCounterparts(params.fs, params.claudeHome, farmListing);
|
|
1625
|
+
const reconciliation = scope.length === 0 ? { actions: [], diagnostics: [], conservative: false } : planReconciliation({
|
|
1626
|
+
...previousManifest === void 0 ? {} : { manifest: previousManifest },
|
|
1627
|
+
farmListing,
|
|
1628
|
+
canonicalListing
|
|
1629
|
+
});
|
|
1630
|
+
const nowMs = params.now();
|
|
1631
|
+
const outcome = executeReconciliation({
|
|
1632
|
+
fs: params.fs,
|
|
1633
|
+
farmRoot,
|
|
1634
|
+
claudeHome: params.claudeHome,
|
|
1635
|
+
actions: reconciliation.actions,
|
|
1636
|
+
nowMs,
|
|
1637
|
+
classification: params.classification
|
|
1638
|
+
});
|
|
1639
|
+
const facts = buildEntryFacts({
|
|
1640
|
+
fs: params.fs,
|
|
1641
|
+
claudeHome: params.claudeHome,
|
|
1642
|
+
home: params.home,
|
|
1643
|
+
cwd: params.cwd,
|
|
1644
|
+
nowMs,
|
|
1645
|
+
env: params.env,
|
|
1646
|
+
...params.branch === void 0 ? {} : { branch: params.branch },
|
|
1647
|
+
...params.branchDetached === void 0 ? {} : { branchDetached: params.branchDetached }
|
|
1648
|
+
});
|
|
1649
|
+
const resolved = resolveDecisions({ facts, cascade: params.cascade, classification: params.classification });
|
|
1650
|
+
const manifest = {
|
|
1651
|
+
version: 1,
|
|
1652
|
+
builtAtMs: nowMs,
|
|
1653
|
+
identity: params.identity,
|
|
1654
|
+
...params.configProfile === void 0 ? {} : { configProfile: params.configProfile },
|
|
1655
|
+
cwd: params.cwd,
|
|
1656
|
+
claudeHome: params.claudeHome,
|
|
1657
|
+
materialised: resolved.farm.materialised,
|
|
1658
|
+
links: resolved.farm.links
|
|
1659
|
+
};
|
|
1660
|
+
const diagnostics = [
|
|
1661
|
+
...resolved.diagnostics,
|
|
1662
|
+
...reconciliation.diagnostics,
|
|
1663
|
+
...outcome.diagnostics,
|
|
1664
|
+
...recoveryDiagnostics(recovery, params.identity)
|
|
1665
|
+
];
|
|
1666
|
+
const farmExists = params.fs.lstat(farmRoot) !== void 0;
|
|
1667
|
+
const nothingAdopted = outcome.adopted.length === 0 && outcome.conflicts.length === 0;
|
|
1668
|
+
if (farmExists && !recovery.recovered && nothingAdopted && previousManifest !== void 0 && farmAlreadyMatches(previousManifest, manifest)) {
|
|
1669
|
+
return {
|
|
1670
|
+
farmRoot,
|
|
1671
|
+
noOp: true,
|
|
1672
|
+
manifest: previousManifest,
|
|
1673
|
+
resolved,
|
|
1674
|
+
adopted: outcome.adopted,
|
|
1675
|
+
recovery,
|
|
1676
|
+
diagnostics
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
const scratchRoot = path4.join(params.identitiesDir, `.${params.identity}.scratch.${params.uniqueSuffix}`);
|
|
1680
|
+
const previousRoot = path4.join(params.identitiesDir, `.${params.identity}.previous.${params.uniqueSuffix}`);
|
|
1681
|
+
buildScratchTree({ fs: params.fs, scratchRoot, plan: resolved.farm, manifest });
|
|
1682
|
+
const swap = swapIn({ fs: params.fs, farmRoot, scratchRoot, previousRoot, classification: params.classification });
|
|
1683
|
+
if (swap.retainedPrevious !== void 0) {
|
|
1684
|
+
diagnostics.push({
|
|
1685
|
+
code: "FARM_PREVIOUS_RETAINED",
|
|
1686
|
+
severity: "warning",
|
|
1687
|
+
message: `The superseded farm was left at ${swap.retainedPrevious} because it still holds ${swap.collided.join(", ")}, which the new farm has its own entry for. Nothing was overwritten in either direction; run \`claude-use identity resolve-conflicts ${params.identity}\` to resolve it interactively.`,
|
|
1688
|
+
subject: swap.retainedPrevious
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
return {
|
|
1692
|
+
farmRoot,
|
|
1693
|
+
noOp: false,
|
|
1694
|
+
manifest,
|
|
1695
|
+
resolved,
|
|
1696
|
+
adopted: outcome.adopted,
|
|
1697
|
+
recovery,
|
|
1698
|
+
diagnostics,
|
|
1699
|
+
...swap.retainedPrevious === void 0 ? {} : { retainedPrevious: swap.retainedPrevious }
|
|
1700
|
+
};
|
|
1701
|
+
} finally {
|
|
1702
|
+
lock.release();
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
function recoveryDiagnostics(recovery, identity) {
|
|
1706
|
+
if (!recovery.recovered) {
|
|
1707
|
+
return [];
|
|
1708
|
+
}
|
|
1709
|
+
const parts = [];
|
|
1710
|
+
if (recovery.removedScratch.length > 0) {
|
|
1711
|
+
parts.push(`removed ${String(recovery.removedScratch.length)} abandoned scratch tree(s)`);
|
|
1712
|
+
}
|
|
1713
|
+
if (recovery.restoredFrom !== void 0) {
|
|
1714
|
+
parts.push(`restored the farm from ${recovery.restoredFrom}, which a previous launch was killed mid-swap`);
|
|
1715
|
+
}
|
|
1716
|
+
if (recovery.completed.length > 0) {
|
|
1717
|
+
parts.push(`finished carrying local state out of ${String(recovery.completed.length)} superseded farm(s)`);
|
|
1718
|
+
}
|
|
1719
|
+
if (recovery.autoResolved.length > 0) {
|
|
1720
|
+
parts.push(`discarded ${String(recovery.autoResolved.length)} superseded runtime entr${recovery.autoResolved.length === 1 ? "y" : "ies"} (${recovery.autoResolved.join(", ")}) \u2014 disposable per-machine state, safe to drop without asking`);
|
|
1721
|
+
}
|
|
1722
|
+
if (recovery.retained.length > 0) {
|
|
1723
|
+
parts.push(
|
|
1724
|
+
`kept ${recovery.retained.join(", ")}, which still holds data the current farm also has an entry for \u2014 run \`claude-use identity resolve-conflicts ${identity}\` to resolve it interactively`
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
return [
|
|
1728
|
+
{
|
|
1729
|
+
code: "FARM_SWAP_RECOVERED",
|
|
1730
|
+
severity: "warning",
|
|
1731
|
+
message: `A previous launch left this identity's farm mid-swap. This launch ${parts.join("; ")}.`
|
|
1732
|
+
}
|
|
1733
|
+
];
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
// src/frontdoor/connect.ts
|
|
1737
|
+
import * as fs from "node:fs";
|
|
1738
|
+
import * as http from "node:http";
|
|
1739
|
+
import * as https from "node:https";
|
|
1740
|
+
import * as net from "node:net";
|
|
1741
|
+
import * as tls from "node:tls";
|
|
1742
|
+
import forge from "node-forge";
|
|
1743
|
+
|
|
1744
|
+
// src/frontdoor/capability.ts
|
|
1745
|
+
var CONNECT_PROXY_REALM = "claude-use front door";
|
|
1746
|
+
var BASIC_SCHEME = "basic";
|
|
1747
|
+
function capabilityFromProxyAuthorization(value) {
|
|
1748
|
+
const trimmed = value.trim();
|
|
1749
|
+
const space = trimmed.indexOf(" ");
|
|
1750
|
+
if (space === -1 || trimmed.slice(0, space).toLowerCase() !== BASIC_SCHEME) {
|
|
1751
|
+
return void 0;
|
|
1752
|
+
}
|
|
1753
|
+
const userPass = Buffer.from(trimmed.slice(space + 1).trim(), "base64").toString("utf8");
|
|
1754
|
+
const colon = userPass.indexOf(":");
|
|
1755
|
+
if (colon === -1) {
|
|
1756
|
+
return void 0;
|
|
1757
|
+
}
|
|
1758
|
+
const password = userPass.slice(colon + 1);
|
|
1759
|
+
return password === "" ? void 0 : password;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// src/frontdoor/state.ts
|
|
1763
|
+
import path5 from "node:path";
|
|
1764
|
+
import { z as z3 } from "zod";
|
|
1765
|
+
var FrontDoorSessionSchema = z3.strictObject({ pid: z3.number().int().positive(), startedAt: z3.number(), token: z3.string().min(1) });
|
|
1766
|
+
var FrontDoorStateSchema = z3.strictObject({
|
|
1767
|
+
/** The supervisor process serving the front door. Alive means both listeners are up in that process. */
|
|
1768
|
+
supervisorPid: z3.number().int().positive().optional(),
|
|
1769
|
+
/** The loopback port the HTTPS provider listener serves on. Absent until the listener has bound and answered its health probe, so "port is set" is itself the ready signal a launcher polls for. */
|
|
1770
|
+
port: z3.number().int().positive().optional(),
|
|
1771
|
+
/**
|
|
1772
|
+
* The sticky port preference for the provider listener: kept across crashes, restarts and idle shutdowns so every provider session, whose base URL was frozen at launch, keeps finding the front door at the same address.
|
|
1773
|
+
*/
|
|
1774
|
+
lastPort: z3.number().int().positive().optional(),
|
|
1775
|
+
/**
|
|
1776
|
+
* The loopback port the CONNECT surface listens on. Absent whenever no supervisor is serving it, so "connectPort is set" is the ready signal an OAuth launch polls for alongside `port`.
|
|
1777
|
+
*/
|
|
1778
|
+
connectPort: z3.number().int().positive().optional(),
|
|
1779
|
+
/**
|
|
1780
|
+
* The sticky port preference for the CONNECT surface, the exact analogue of `lastPort` for the second listener: it survives every shutdown, because every OAuth session's environment was frozen at launch with HTTPS_PROXY pointing at this address and a restart that moves strands them.
|
|
1781
|
+
*/
|
|
1782
|
+
lastConnectPort: z3.number().int().positive().optional(),
|
|
1783
|
+
/**
|
|
1784
|
+
* The loopback port the direct listener serves on: the listener a headroom hop tells the daemon to forward routed traffic back to. Sticky like the others, because headroom's allowlist admits its exact origin and an ephemeral port would invalidate the allowlist on every restart.
|
|
1785
|
+
*/
|
|
1786
|
+
directPort: z3.number().int().positive().optional(),
|
|
1787
|
+
/** The sticky port preference for the direct listener, for the reason `directPort` gives. */
|
|
1788
|
+
lastDirectPort: z3.number().int().positive().optional(),
|
|
1789
|
+
/** The last fatal error, kept across shutdowns so `frontdoor status` can explain a front door that is not running. */
|
|
1790
|
+
lastError: z3.string().optional()
|
|
1791
|
+
});
|
|
1792
|
+
function readFrontDoorState(fs2, stateFile) {
|
|
1793
|
+
let raw;
|
|
1794
|
+
try {
|
|
1795
|
+
raw = fs2.readFileUtf8(stateFile);
|
|
1796
|
+
} catch {
|
|
1797
|
+
return void 0;
|
|
1798
|
+
}
|
|
1799
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
1800
|
+
return void 0;
|
|
1801
|
+
}
|
|
1802
|
+
try {
|
|
1803
|
+
const parsed = FrontDoorStateSchema.safeParse(JSON.parse(raw));
|
|
1804
|
+
return parsed.success ? parsed.data : void 0;
|
|
1805
|
+
} catch {
|
|
1806
|
+
return void 0;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function writeFrontDoorState(fs2, stateFile, state) {
|
|
1810
|
+
fs2.mkdirp(path5.dirname(stateFile));
|
|
1811
|
+
fs2.writeFileUtf8(stateFile, `${JSON.stringify(state, null, 2)}
|
|
1812
|
+
`);
|
|
1813
|
+
}
|
|
1814
|
+
function frontDoorOrigin(state) {
|
|
1815
|
+
const port = state?.directPort ?? state?.lastDirectPort;
|
|
1816
|
+
return port === void 0 ? void 0 : `http://127.0.0.1:${String(port)}`;
|
|
1817
|
+
}
|
|
1818
|
+
function writeFrontDoorSession(fs2, sessionsDir, session) {
|
|
1819
|
+
fs2.mkdirPrivate(sessionsDir);
|
|
1820
|
+
fs2.writeFilePrivate(path5.join(sessionsDir, `${String(session.pid)}.json`), `${JSON.stringify(session, null, 2)}
|
|
1821
|
+
`);
|
|
1822
|
+
}
|
|
1823
|
+
function listFrontDoorSessions(fs2, sessionsDir) {
|
|
1824
|
+
const sessions = [];
|
|
1825
|
+
for (const name of fs2.readdir(sessionsDir)) {
|
|
1826
|
+
if (!name.endsWith(".json")) {
|
|
1827
|
+
continue;
|
|
1828
|
+
}
|
|
1829
|
+
try {
|
|
1830
|
+
const parsed = FrontDoorSessionSchema.safeParse(JSON.parse(fs2.readFileUtf8(path5.join(sessionsDir, name)) ?? ""));
|
|
1831
|
+
if (parsed.success) {
|
|
1832
|
+
sessions.push({ pid: parsed.data.pid, startedAt: parsed.data.startedAt });
|
|
1833
|
+
}
|
|
1834
|
+
} catch {
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return sessions.sort((a, b) => a.pid - b.pid);
|
|
1839
|
+
}
|
|
1840
|
+
function removeFrontDoorSession(fs2, sessionsDir, pid) {
|
|
1841
|
+
fs2.removeRecursive(path5.join(sessionsDir, `${String(pid)}.json`));
|
|
1842
|
+
}
|
|
1843
|
+
function pruneDeadFrontDoorSessions(fs2, sessionsDir, isRunning) {
|
|
1844
|
+
const removed = [];
|
|
1845
|
+
for (const session of listFrontDoorSessions(fs2, sessionsDir)) {
|
|
1846
|
+
if (!isRunning(session.pid)) {
|
|
1847
|
+
removeFrontDoorSession(fs2, sessionsDir, session.pid);
|
|
1848
|
+
removed.push(session.pid);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return removed;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/frontdoor/supervisor.ts
|
|
1855
|
+
var FRONTDOOR_SUPERVISOR_STILL_RUNNING = -1;
|
|
1856
|
+
var FRONTDOOR_POLL_MS = 1e3;
|
|
1857
|
+
var MS_PER_MINUTE = 6e4;
|
|
1858
|
+
async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {}) {
|
|
1859
|
+
const { fs: fs2, paths } = ports;
|
|
1860
|
+
const previous = readFrontDoorState(fs2, paths.frontdoorStateFile);
|
|
1861
|
+
let lastPort = previous?.lastPort;
|
|
1862
|
+
let lastConnectPort = previous?.lastConnectPort;
|
|
1863
|
+
let lastDirectPort = previous?.lastDirectPort;
|
|
1864
|
+
const sticky = () => ({
|
|
1865
|
+
...lastPort === void 0 ? {} : { lastPort },
|
|
1866
|
+
...lastConnectPort === void 0 ? {} : { lastConnectPort },
|
|
1867
|
+
...lastDirectPort === void 0 ? {} : { lastDirectPort }
|
|
1868
|
+
});
|
|
1869
|
+
const fail = (message) => {
|
|
1870
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { lastError: message, ...sticky() });
|
|
1871
|
+
fs2.removeRecursive(paths.frontdoorLockFile);
|
|
1872
|
+
ports.log(`claude-use frontdoor supervisor: ${message}`);
|
|
1873
|
+
return 1;
|
|
1874
|
+
};
|
|
1875
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, ...sticky() });
|
|
1876
|
+
const previousSticky = lastPort;
|
|
1877
|
+
let http2;
|
|
1878
|
+
try {
|
|
1879
|
+
http2 = await ports.startProviderListener(lastPort);
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
return fail(`could not start the provider listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
1882
|
+
}
|
|
1883
|
+
if (previousSticky !== void 0 && http2.port !== previousSticky) {
|
|
1884
|
+
ports.log(`claude-use frontdoor supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(http2.port)} (sessions launched against the old port are stale until they relaunch)`);
|
|
1885
|
+
}
|
|
1886
|
+
lastPort = http2.port;
|
|
1887
|
+
const previousStickyConnect = lastConnectPort;
|
|
1888
|
+
let connect2;
|
|
1889
|
+
try {
|
|
1890
|
+
connect2 = await ports.startConnectListener(lastConnectPort);
|
|
1891
|
+
} catch (error) {
|
|
1892
|
+
await http2.close();
|
|
1893
|
+
return fail(`could not start the CONNECT surface: ${error instanceof Error ? error.message : String(error)}`);
|
|
1894
|
+
}
|
|
1895
|
+
if (previousStickyConnect !== void 0 && connect2.port !== previousStickyConnect) {
|
|
1896
|
+
ports.log(`claude-use frontdoor supervisor: sticky CONNECT port ${String(previousStickyConnect)} was occupied; moving to ${String(connect2.port)} (OAuth sessions launched against the old port are stale until they relaunch)`);
|
|
1897
|
+
}
|
|
1898
|
+
lastConnectPort = connect2.port;
|
|
1899
|
+
const previousStickyDirect = lastDirectPort;
|
|
1900
|
+
let direct;
|
|
1901
|
+
try {
|
|
1902
|
+
direct = await ports.startDirectListener(lastDirectPort);
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
await connect2.close();
|
|
1905
|
+
await http2.close();
|
|
1906
|
+
return fail(`could not start the direct listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
1907
|
+
}
|
|
1908
|
+
if (previousStickyDirect !== void 0 && direct.port !== previousStickyDirect) {
|
|
1909
|
+
ports.log(`claude-use frontdoor supervisor: sticky direct port ${String(previousStickyDirect)} was occupied; moving to ${String(direct.port)} (headroom must be restarted to admit the new address)`);
|
|
1910
|
+
}
|
|
1911
|
+
lastDirectPort = direct.port;
|
|
1912
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, port: http2.port, connectPort: connect2.port, directPort: direct.port, ...sticky() });
|
|
1913
|
+
fs2.removeRecursive(paths.frontdoorLockFile);
|
|
1914
|
+
ports.log(`claude-use frontdoor supervisor ${String(ports.ownPid)}: front door on 127.0.0.1:${String(http2.port)}, CONNECT surface on 127.0.0.1:${String(connect2.port)}`);
|
|
1915
|
+
let idleSince;
|
|
1916
|
+
let ticks = 0;
|
|
1917
|
+
for (; ; ) {
|
|
1918
|
+
if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
|
|
1919
|
+
return FRONTDOOR_SUPERVISOR_STILL_RUNNING;
|
|
1920
|
+
}
|
|
1921
|
+
ticks += 1;
|
|
1922
|
+
pruneDeadFrontDoorSessions(fs2, paths.frontdoorSessionsDir, ports.isRunning);
|
|
1923
|
+
if (listFrontDoorSessions(fs2, paths.frontdoorSessionsDir).length === 0) {
|
|
1924
|
+
idleSince ??= ports.now();
|
|
1925
|
+
if (ports.now() - idleSince >= idleShutdownMinutes * MS_PER_MINUTE) {
|
|
1926
|
+
ports.log(`claude-use frontdoor supervisor: no sessions for ${String(idleShutdownMinutes)} minute(s); closing the front door and exiting`);
|
|
1927
|
+
await direct.close();
|
|
1928
|
+
await connect2.close();
|
|
1929
|
+
await http2.close();
|
|
1930
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, sticky());
|
|
1931
|
+
return 0;
|
|
1932
|
+
}
|
|
1933
|
+
} else {
|
|
1934
|
+
idleSince = void 0;
|
|
1935
|
+
}
|
|
1936
|
+
await ports.sleep(FRONTDOOR_POLL_MS);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/frontdoor/connect.ts
|
|
1941
|
+
var CONNECT_INTERCEPT_HOST = "api.anthropic.com";
|
|
1942
|
+
var ROUTED_PATH_PREFIX = "/v1/";
|
|
1943
|
+
var HTTPS_PORT = 443;
|
|
1944
|
+
var MAX_PORT = 65535;
|
|
1945
|
+
var SERIAL_NUMBER_BYTES = 16;
|
|
1946
|
+
var HTTP_BAD_GATEWAY = 502;
|
|
1947
|
+
var CA_KEY_FILE_MODE = 384;
|
|
1948
|
+
var CONNECT_LINE_TOKENS = 3;
|
|
1949
|
+
var HEAD_TERMINATOR = "\r\n\r\n";
|
|
1950
|
+
var MAX_CONNECT_HEAD_BYTES = 8192;
|
|
1951
|
+
var CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
|
|
1952
|
+
var REFUSAL = {
|
|
1953
|
+
badRequest: "400 Bad Request",
|
|
1954
|
+
proxyAuthenticationRequired: "407 Proxy Authentication Required",
|
|
1955
|
+
requestTimeout: "408 Request Timeout",
|
|
1956
|
+
headTooLarge: "431 Request Header Fields Too Large",
|
|
1957
|
+
serviceUnavailable: "503 Service Unavailable"
|
|
1958
|
+
};
|
|
1959
|
+
var CONNECT_LIMITS = {
|
|
1960
|
+
headDeadlineMs: 1e4,
|
|
1961
|
+
maxPendingHeads: 64,
|
|
1962
|
+
maxTunnels: 1024,
|
|
1963
|
+
revalidateMs: FRONTDOOR_POLL_MS
|
|
1964
|
+
};
|
|
1965
|
+
var CA_VALIDITY_DAYS = 3650;
|
|
1966
|
+
var LEAF_VALIDITY_DAYS = 397;
|
|
1967
|
+
var CA_SUBJECT_COMMON_NAME = "claude-use front door CA";
|
|
1968
|
+
var MS_PER_DAY = 864e5;
|
|
1969
|
+
function parseConnectTarget(authority) {
|
|
1970
|
+
const colon = authority.lastIndexOf(":");
|
|
1971
|
+
if (colon === -1) {
|
|
1972
|
+
const host2 = authority.trim().toLowerCase();
|
|
1973
|
+
return host2 === "" ? void 0 : { host: host2, port: HTTPS_PORT };
|
|
1974
|
+
}
|
|
1975
|
+
const host = authority.slice(0, colon).trim().toLowerCase();
|
|
1976
|
+
const port = Number.parseInt(authority.slice(colon + 1), 10);
|
|
1977
|
+
if (host === "" || !Number.isInteger(port) || port <= 0 || port > MAX_PORT) {
|
|
1978
|
+
return void 0;
|
|
1979
|
+
}
|
|
1980
|
+
return { host, port };
|
|
1981
|
+
}
|
|
1982
|
+
function isInterceptedHost(host, interceptHost) {
|
|
1983
|
+
return host === interceptHost;
|
|
1984
|
+
}
|
|
1985
|
+
function servedByPipeline(path7) {
|
|
1986
|
+
return path7?.startsWith(ROUTED_PATH_PREFIX) === true;
|
|
1987
|
+
}
|
|
1988
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
1989
|
+
"connection",
|
|
1990
|
+
"keep-alive",
|
|
1991
|
+
"proxy-connection",
|
|
1992
|
+
"proxy-authenticate",
|
|
1993
|
+
"proxy-authorization",
|
|
1994
|
+
"te",
|
|
1995
|
+
"trailer",
|
|
1996
|
+
"transfer-encoding",
|
|
1997
|
+
"upgrade"
|
|
1998
|
+
]);
|
|
1999
|
+
function forwardableHeaders(headers) {
|
|
2000
|
+
const connectionTokens = new Set(
|
|
2001
|
+
(headers.connection ?? "").split(",").map((token) => token.trim().toLowerCase()).filter((token) => token !== "")
|
|
2002
|
+
);
|
|
2003
|
+
const result = {};
|
|
2004
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
2005
|
+
if (value === void 0) {
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
const lower = name.toLowerCase();
|
|
2009
|
+
if (HOP_BY_HOP_HEADERS.has(lower) || connectionTokens.has(lower)) {
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
result[name] = value;
|
|
2013
|
+
}
|
|
2014
|
+
return result;
|
|
2015
|
+
}
|
|
2016
|
+
function generateCa(now) {
|
|
2017
|
+
const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 });
|
|
2018
|
+
const cert = forge.pki.createCertificate();
|
|
2019
|
+
cert.publicKey = keys.publicKey;
|
|
2020
|
+
cert.serialNumber = forge.util.bytesToHex(forge.random.getBytesSync(SERIAL_NUMBER_BYTES));
|
|
2021
|
+
cert.validity.notBefore = new Date(now.getTime() - MS_PER_DAY);
|
|
2022
|
+
cert.validity.notAfter = new Date(now.getTime() + CA_VALIDITY_DAYS * MS_PER_DAY);
|
|
2023
|
+
const subject = [{ name: "commonName", value: CA_SUBJECT_COMMON_NAME }];
|
|
2024
|
+
cert.setSubject(subject);
|
|
2025
|
+
cert.setIssuer(subject);
|
|
2026
|
+
cert.setExtensions([
|
|
2027
|
+
{ name: "basicConstraints", cA: true, critical: true },
|
|
2028
|
+
{ name: "keyUsage", keyCertSign: true, cRLSign: true, critical: true },
|
|
2029
|
+
{ name: "subjectKeyIdentifier" }
|
|
2030
|
+
]);
|
|
2031
|
+
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
2032
|
+
return { certPem: forge.pki.certificateToPem(cert), keyPem: forge.pki.privateKeyToPem(keys.privateKey) };
|
|
2033
|
+
}
|
|
2034
|
+
var SAN_TYPE_DNS = 2;
|
|
2035
|
+
var SAN_TYPE_IP = 7;
|
|
2036
|
+
function mintLeaf(ca, names, now) {
|
|
2037
|
+
const [commonName] = names;
|
|
2038
|
+
if (commonName === void 0) {
|
|
2039
|
+
throw new Error("a leaf certificate needs at least one name");
|
|
2040
|
+
}
|
|
2041
|
+
const caCert = forge.pki.certificateFromPem(ca.certPem);
|
|
2042
|
+
const caKey = forge.pki.privateKeyFromPem(ca.keyPem);
|
|
2043
|
+
const keys = forge.pki.rsa.generateKeyPair({ bits: 2048 });
|
|
2044
|
+
const cert = forge.pki.createCertificate();
|
|
2045
|
+
cert.publicKey = keys.publicKey;
|
|
2046
|
+
cert.serialNumber = forge.util.bytesToHex(forge.random.getBytesSync(SERIAL_NUMBER_BYTES));
|
|
2047
|
+
cert.validity.notBefore = new Date(now.getTime() - MS_PER_DAY);
|
|
2048
|
+
cert.validity.notAfter = new Date(now.getTime() + LEAF_VALIDITY_DAYS * MS_PER_DAY);
|
|
2049
|
+
cert.setSubject([{ name: "commonName", value: commonName }]);
|
|
2050
|
+
cert.setIssuer(caCert.subject.attributes);
|
|
2051
|
+
cert.setExtensions([
|
|
2052
|
+
{ name: "basicConstraints", cA: false, critical: true },
|
|
2053
|
+
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true, critical: true },
|
|
2054
|
+
{ name: "extKeyUsage", serverAuth: true },
|
|
2055
|
+
{ name: "subjectAltName", altNames: names.map((name) => net.isIP(name) === 0 ? { type: SAN_TYPE_DNS, value: name } : { type: SAN_TYPE_IP, ip: name }) },
|
|
2056
|
+
{ name: "subjectKeyIdentifier" },
|
|
2057
|
+
{ name: "authorityKeyIdentifier", keyLocatorFields: [] }
|
|
2058
|
+
]);
|
|
2059
|
+
cert.sign(caKey, forge.md.sha256.create());
|
|
2060
|
+
return { certPem: forge.pki.certificateToPem(cert), keyPem: forge.pki.privateKeyToPem(keys.privateKey) };
|
|
2061
|
+
}
|
|
2062
|
+
function createLeafCache(ca, now) {
|
|
2063
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2064
|
+
return (host) => {
|
|
2065
|
+
const existing = cache.get(host);
|
|
2066
|
+
if (existing !== void 0) {
|
|
2067
|
+
return existing;
|
|
2068
|
+
}
|
|
2069
|
+
const minted = mintLeaf(ca, [host], now());
|
|
2070
|
+
cache.set(host, minted);
|
|
2071
|
+
return minted;
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
function ensureCa(store, generate) {
|
|
2075
|
+
const existing = store.loadCa();
|
|
2076
|
+
if (existing !== void 0 && caParses(existing)) {
|
|
2077
|
+
return existing;
|
|
2078
|
+
}
|
|
2079
|
+
const ca = generate();
|
|
2080
|
+
store.writeCa(ca);
|
|
2081
|
+
return ca;
|
|
2082
|
+
}
|
|
2083
|
+
function caParses(ca) {
|
|
2084
|
+
try {
|
|
2085
|
+
forge.pki.certificateFromPem(ca.certPem);
|
|
2086
|
+
forge.pki.privateKeyFromPem(ca.keyPem);
|
|
2087
|
+
return true;
|
|
2088
|
+
} catch {
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
async function startConnectServer(config, effects, preferredPort) {
|
|
2093
|
+
const handler = (request3, response) => {
|
|
2094
|
+
if (servedByPipeline(request3.url)) {
|
|
2095
|
+
config.serveRouted(request3, response);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
effects.forwardHttp(config.upstream, request3, response);
|
|
2099
|
+
};
|
|
2100
|
+
const httpSession = effects.createHttpSession(handler);
|
|
2101
|
+
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(config.interceptHost), (secure) => {
|
|
2102
|
+
httpSession.serve(secure);
|
|
2103
|
+
});
|
|
2104
|
+
const ledger = createConnectionLedger(config.limits, config.isLiveCapability);
|
|
2105
|
+
const listener = await effects.listenLoopback(preferredPort, (socket) => {
|
|
2106
|
+
handleConnect(socket, { config, effects, tlsAcceptor, ledger });
|
|
2107
|
+
});
|
|
2108
|
+
return {
|
|
2109
|
+
port: listener.port,
|
|
2110
|
+
close: async () => {
|
|
2111
|
+
ledger.close();
|
|
2112
|
+
await listener.close();
|
|
2113
|
+
tlsAcceptor.close();
|
|
2114
|
+
httpSession.close();
|
|
2115
|
+
}
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
function createConnectionLedger(limits, isLiveCapability) {
|
|
2119
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2120
|
+
const authenticated = /* @__PURE__ */ new Map();
|
|
2121
|
+
let tunnels = 0;
|
|
2122
|
+
const leavePending = (socket) => {
|
|
2123
|
+
const deadline = pending.get(socket);
|
|
2124
|
+
if (deadline !== void 0) {
|
|
2125
|
+
clearTimeout(deadline);
|
|
2126
|
+
pending.delete(socket);
|
|
2127
|
+
}
|
|
2128
|
+
};
|
|
2129
|
+
const revalidation = setInterval(() => {
|
|
2130
|
+
for (const [capability, sockets] of authenticated) {
|
|
2131
|
+
if (!isLiveCapability(capability)) {
|
|
2132
|
+
for (const socket of sockets) {
|
|
2133
|
+
socket.destroy();
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}, limits.revalidateMs);
|
|
2138
|
+
revalidation.unref();
|
|
2139
|
+
return {
|
|
2140
|
+
accept: (socket) => {
|
|
2141
|
+
const [oldest] = pending.keys();
|
|
2142
|
+
if (pending.size >= limits.maxPendingHeads && oldest !== void 0) {
|
|
2143
|
+
leavePending(oldest);
|
|
2144
|
+
refuse(oldest, REFUSAL.requestTimeout);
|
|
2145
|
+
}
|
|
2146
|
+
pending.set(
|
|
2147
|
+
socket,
|
|
2148
|
+
setTimeout(() => {
|
|
2149
|
+
leavePending(socket);
|
|
2150
|
+
refuse(socket, REFUSAL.requestTimeout);
|
|
2151
|
+
}, limits.headDeadlineMs)
|
|
2152
|
+
);
|
|
2153
|
+
socket.once("close", () => {
|
|
2154
|
+
leavePending(socket);
|
|
2155
|
+
});
|
|
2156
|
+
},
|
|
2157
|
+
headReceived: leavePending,
|
|
2158
|
+
establish: (socket, capability) => {
|
|
2159
|
+
if (tunnels >= limits.maxTunnels) {
|
|
2160
|
+
return false;
|
|
2161
|
+
}
|
|
2162
|
+
tunnels += 1;
|
|
2163
|
+
const sockets = authenticated.get(capability) ?? /* @__PURE__ */ new Set();
|
|
2164
|
+
sockets.add(socket);
|
|
2165
|
+
authenticated.set(capability, sockets);
|
|
2166
|
+
socket.once("close", () => {
|
|
2167
|
+
tunnels -= 1;
|
|
2168
|
+
sockets.delete(socket);
|
|
2169
|
+
if (sockets.size === 0 && authenticated.get(capability) === sockets) {
|
|
2170
|
+
authenticated.delete(capability);
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
return true;
|
|
2174
|
+
},
|
|
2175
|
+
close: () => {
|
|
2176
|
+
clearInterval(revalidation);
|
|
2177
|
+
for (const deadline of pending.values()) {
|
|
2178
|
+
clearTimeout(deadline);
|
|
2179
|
+
}
|
|
2180
|
+
pending.clear();
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
function refuse(socket, status, headers = []) {
|
|
2185
|
+
socket.removeAllListeners("data");
|
|
2186
|
+
socket.pause();
|
|
2187
|
+
if (socket.destroyed || socket.writableEnded) {
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
socket.end(`HTTP/1.1 ${status}\r
|
|
2191
|
+
${headers.map((header) => `${header}\r
|
|
2192
|
+
`).join("")}content-length: 0\r
|
|
2193
|
+
connection: close\r
|
|
2194
|
+
\r
|
|
2195
|
+
`, () => {
|
|
2196
|
+
socket.destroy();
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
function handleConnect(socket, context) {
|
|
2200
|
+
socket.on("error", () => {
|
|
2201
|
+
socket.destroy();
|
|
2202
|
+
});
|
|
2203
|
+
context.ledger.accept(socket);
|
|
2204
|
+
let buffer = Buffer.alloc(0);
|
|
2205
|
+
const onData = (chunk) => {
|
|
2206
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
2207
|
+
const headEnd = buffer.indexOf(HEAD_TERMINATOR);
|
|
2208
|
+
if (headEnd === -1 ? buffer.length > MAX_CONNECT_HEAD_BYTES : headEnd > MAX_CONNECT_HEAD_BYTES) {
|
|
2209
|
+
context.ledger.headReceived(socket);
|
|
2210
|
+
refuse(socket, REFUSAL.headTooLarge);
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
if (headEnd === -1) {
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
socket.removeListener("data", onData);
|
|
2217
|
+
socket.pause();
|
|
2218
|
+
context.ledger.headReceived(socket);
|
|
2219
|
+
const head = buffer.subarray(0, headEnd).toString("utf8");
|
|
2220
|
+
socket.unshift(buffer.subarray(headEnd + HEAD_TERMINATOR.length));
|
|
2221
|
+
routeConnect(socket, head, context);
|
|
2222
|
+
};
|
|
2223
|
+
socket.on("data", onData);
|
|
2224
|
+
}
|
|
2225
|
+
function proxyAuthorizationOf(head) {
|
|
2226
|
+
const values = head.split("\r\n").slice(1).flatMap((line) => {
|
|
2227
|
+
const colon = line.indexOf(":");
|
|
2228
|
+
return colon > 0 && line.slice(0, colon).trim().toLowerCase() === "proxy-authorization" ? [line.slice(colon + 1)] : [];
|
|
2229
|
+
});
|
|
2230
|
+
const [value] = values;
|
|
2231
|
+
return values.length === 1 ? value : void 0;
|
|
2232
|
+
}
|
|
2233
|
+
function routeConnect(socket, head, context) {
|
|
2234
|
+
const { config, effects, tlsAcceptor, ledger } = context;
|
|
2235
|
+
const credential = proxyAuthorizationOf(head);
|
|
2236
|
+
const capability = credential === void 0 ? void 0 : capabilityFromProxyAuthorization(credential);
|
|
2237
|
+
if (capability === void 0 || !config.isLiveCapability(capability)) {
|
|
2238
|
+
refuse(socket, REFUSAL.proxyAuthenticationRequired, [`Proxy-Authenticate: Basic realm="${CONNECT_PROXY_REALM}"`]);
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
const requestLine = head.split("\r\n", 1)[0] ?? "";
|
|
2242
|
+
const parts = requestLine.split(" ");
|
|
2243
|
+
const authority = parts.length === CONNECT_LINE_TOKENS && parts[0]?.toUpperCase() === "CONNECT" ? parts[1] ?? "" : void 0;
|
|
2244
|
+
const target = authority === void 0 ? void 0 : parseConnectTarget(authority);
|
|
2245
|
+
if (target === void 0) {
|
|
2246
|
+
refuse(socket, REFUSAL.badRequest);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
if (!ledger.establish(socket, capability)) {
|
|
2250
|
+
refuse(socket, REFUSAL.serviceUnavailable);
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
socket.write(CONNECT_ESTABLISHED);
|
|
2254
|
+
if (isInterceptedHost(target.host, config.interceptHost)) {
|
|
2255
|
+
tlsAcceptor.accept(socket);
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
void blindTunnel(socket, target, effects);
|
|
2259
|
+
}
|
|
2260
|
+
async function blindTunnel(socket, target, effects) {
|
|
2261
|
+
let upstream;
|
|
2262
|
+
try {
|
|
2263
|
+
upstream = await effects.connectTcp(target.host, target.port);
|
|
2264
|
+
} catch {
|
|
2265
|
+
socket.destroy();
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
if (socket.destroyed) {
|
|
2269
|
+
upstream.destroy();
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
socket.pipe(upstream);
|
|
2273
|
+
upstream.pipe(socket);
|
|
2274
|
+
const drop = () => {
|
|
2275
|
+
socket.destroy();
|
|
2276
|
+
upstream.destroy();
|
|
2277
|
+
};
|
|
2278
|
+
socket.on("error", drop);
|
|
2279
|
+
upstream.on("error", drop);
|
|
2280
|
+
socket.on("close", () => {
|
|
2281
|
+
upstream.destroy();
|
|
2282
|
+
});
|
|
2283
|
+
upstream.on("close", () => {
|
|
2284
|
+
socket.destroy();
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
function realConnectEffects() {
|
|
2288
|
+
const plainAgent = new http.Agent({ keepAlive: true });
|
|
2289
|
+
const tlsAgent = new https.Agent({ keepAlive: true });
|
|
2290
|
+
const listenOnce = async (port, onSocket) => await new Promise((resolve, reject) => {
|
|
2291
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
2292
|
+
const server = net.createServer((socket) => {
|
|
2293
|
+
accepted.add(socket);
|
|
2294
|
+
socket.on("close", () => {
|
|
2295
|
+
accepted.delete(socket);
|
|
2296
|
+
});
|
|
2297
|
+
onSocket(socket);
|
|
2298
|
+
});
|
|
2299
|
+
server.once("error", reject);
|
|
2300
|
+
server.listen(port, "127.0.0.1", () => {
|
|
2301
|
+
const address = server.address();
|
|
2302
|
+
const bound = typeof address === "object" && address !== null ? address.port : 0;
|
|
2303
|
+
if (bound === 0) {
|
|
2304
|
+
reject(new Error("could not bind a loopback port"));
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
resolve({
|
|
2308
|
+
port: bound,
|
|
2309
|
+
close: async () => {
|
|
2310
|
+
await new Promise((closeResolve) => {
|
|
2311
|
+
server.close(() => {
|
|
2312
|
+
closeResolve(void 0);
|
|
2313
|
+
});
|
|
2314
|
+
for (const socket of accepted) {
|
|
2315
|
+
socket.destroy();
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
});
|
|
2320
|
+
});
|
|
2321
|
+
});
|
|
2322
|
+
return {
|
|
2323
|
+
listenLoopback: async (preferredPort, onSocket) => {
|
|
2324
|
+
if (preferredPort !== void 0) {
|
|
2325
|
+
try {
|
|
2326
|
+
return await listenOnce(preferredPort, onSocket);
|
|
2327
|
+
} catch {
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
return await listenOnce(0, onSocket);
|
|
2331
|
+
},
|
|
2332
|
+
createTlsAcceptor: (leaf, onSecure) => {
|
|
2333
|
+
const server = tls.createServer({ key: leaf.keyPem, cert: leaf.certPem }, (secure) => {
|
|
2334
|
+
onSecure(secure);
|
|
2335
|
+
});
|
|
2336
|
+
server.on("clientError", (_error, socket) => {
|
|
2337
|
+
socket.destroy();
|
|
2338
|
+
});
|
|
2339
|
+
return {
|
|
2340
|
+
// tls.Server is a net.Server whose connection listener wraps the raw socket in the TLS handshake, and emitting the event by hand is what runs that listener on a socket this process already owns (the CONNECT half of the connection) rather than one the server accepted itself.
|
|
2341
|
+
accept: (socket) => {
|
|
2342
|
+
server.emit("connection", socket);
|
|
2343
|
+
},
|
|
2344
|
+
close: () => {
|
|
2345
|
+
server.close();
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
},
|
|
2349
|
+
createHttpSession: (handler) => {
|
|
2350
|
+
const server = http.createServer(handler);
|
|
2351
|
+
server.on("clientError", (_error, socket) => {
|
|
2352
|
+
socket.destroy();
|
|
2353
|
+
});
|
|
2354
|
+
return {
|
|
2355
|
+
serve: (socket) => {
|
|
2356
|
+
server.emit("connection", socket);
|
|
2357
|
+
},
|
|
2358
|
+
close: () => {
|
|
2359
|
+
server.close();
|
|
2360
|
+
server.closeAllConnections();
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
},
|
|
2364
|
+
connectTcp: async (host, port) => await new Promise((resolve, reject) => {
|
|
2365
|
+
const socket = net.connect({ host, port });
|
|
2366
|
+
socket.once("connect", () => {
|
|
2367
|
+
resolve(socket);
|
|
2368
|
+
});
|
|
2369
|
+
socket.once("error", reject);
|
|
2370
|
+
}),
|
|
2371
|
+
forwardHttp: (target, request3, response) => {
|
|
2372
|
+
const headers = forwardableHeaders(request3.headers);
|
|
2373
|
+
const options = {
|
|
2374
|
+
host: target.host,
|
|
2375
|
+
port: target.port,
|
|
2376
|
+
method: request3.method,
|
|
2377
|
+
path: request3.url,
|
|
2378
|
+
headers,
|
|
2379
|
+
agent: target.tls ? tlsAgent : plainAgent
|
|
2380
|
+
};
|
|
2381
|
+
if (target.tls) {
|
|
2382
|
+
options.servername = target.servername ?? target.host;
|
|
2383
|
+
if (target.ca !== void 0) {
|
|
2384
|
+
options.ca = [...target.ca];
|
|
2385
|
+
}
|
|
2386
|
+
if (target.rejectUnauthorized !== void 0) {
|
|
2387
|
+
options.rejectUnauthorized = target.rejectUnauthorized;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
const onUpstreamResponse = (upstreamResponse) => {
|
|
2391
|
+
response.writeHead(upstreamResponse.statusCode ?? HTTP_BAD_GATEWAY, forwardableHeaders(upstreamResponse.headers));
|
|
2392
|
+
response.flushHeaders();
|
|
2393
|
+
upstreamResponse.pipe(response);
|
|
2394
|
+
};
|
|
2395
|
+
const upstream = target.tls ? https.request(options, onUpstreamResponse) : http.request(options, onUpstreamResponse);
|
|
2396
|
+
upstream.on("error", (error) => {
|
|
2397
|
+
if (response.headersSent) {
|
|
2398
|
+
response.destroy(error);
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
const body = `claude-use headroom: upstream unreachable (${error.message})`;
|
|
2402
|
+
response.writeHead(HTTP_BAD_GATEWAY, { "content-type": "text/plain", "content-length": String(body.length) });
|
|
2403
|
+
response.end(body);
|
|
2404
|
+
});
|
|
2405
|
+
request3.pipe(upstream);
|
|
2406
|
+
request3.socket.setNoDelay(true);
|
|
2407
|
+
upstream.on("socket", (socket) => {
|
|
2408
|
+
socket.setNoDelay(true);
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
function isEnoent(error) {
|
|
2414
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2415
|
+
}
|
|
2416
|
+
function realConnectCertStore(paths) {
|
|
2417
|
+
const read = (file) => {
|
|
2418
|
+
try {
|
|
2419
|
+
return fs.readFileSync(file, "utf8");
|
|
2420
|
+
} catch (error) {
|
|
2421
|
+
if (isEnoent(error)) {
|
|
2422
|
+
return void 0;
|
|
2423
|
+
}
|
|
2424
|
+
throw error;
|
|
2425
|
+
}
|
|
2426
|
+
};
|
|
2427
|
+
return {
|
|
2428
|
+
loadCa: () => {
|
|
2429
|
+
const certPem = read(paths.frontdoorCaCertFile);
|
|
2430
|
+
const keyPem = read(paths.frontdoorCaKeyFile);
|
|
2431
|
+
return certPem === void 0 || keyPem === void 0 ? void 0 : { certPem, keyPem };
|
|
2432
|
+
},
|
|
2433
|
+
writeCa: (ca) => {
|
|
2434
|
+
fs.mkdirSync(paths.frontdoorCaDir, { recursive: true });
|
|
2435
|
+
fs.writeFileSync(paths.frontdoorCaCertFile, ca.certPem, "utf8");
|
|
2436
|
+
fs.writeFileSync(paths.frontdoorCaKeyFile, ca.keyPem, { encoding: "utf8", mode: CA_KEY_FILE_MODE });
|
|
2437
|
+
fs.chmodSync(paths.frontdoorCaKeyFile, CA_KEY_FILE_MODE);
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// src/frontdoor/ensure.ts
|
|
2443
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2444
|
+
|
|
2445
|
+
// src/headroom/state.ts
|
|
2446
|
+
import { createHash } from "node:crypto";
|
|
2447
|
+
import path6 from "node:path";
|
|
2448
|
+
import { z as z4 } from "zod";
|
|
2449
|
+
var HEADROOM_ANTHROPIC_UPSTREAM = "https://api.anthropic.com";
|
|
2450
|
+
var HeadroomStateSchema = z4.strictObject({
|
|
2451
|
+
/** The supervisor process owning the daemon. Alive means someone is keeping headroom running. */
|
|
2452
|
+
supervisorPid: z4.number().int().positive().optional(),
|
|
2453
|
+
/** The headroom proxy process itself. Absent while the supervisor is between restarts. */
|
|
2454
|
+
headroomPid: z4.number().int().positive().optional(),
|
|
2455
|
+
/** The loopback port the proxy listens on. Absent until the proxy has passed its readiness check, so "port is set" is itself the ready signal a launcher polls for. */
|
|
2456
|
+
port: z4.number().int().positive().optional(),
|
|
2457
|
+
/**
|
|
2458
|
+
* The sticky port preference: the address the daemon last served on, kept across crashes, restarts, and idle shutdowns so the next start reuses it. Distinct from `port` on purpose: `port` is the ready signal (absent whenever nothing is serving), while `lastPort` survives every shutdown, because the front door (which routes sessions through this daemon) reads it live and follows a restart to whichever address it lands on.
|
|
2459
|
+
*/
|
|
2460
|
+
lastPort: z4.number().int().positive().optional(),
|
|
2461
|
+
/** The `headroom --version` output of the running install. */
|
|
2462
|
+
version: z4.string().optional(),
|
|
2463
|
+
/** Hash of the allowlist the running proxy was started with, so a provider-file change is detected as drift. */
|
|
2464
|
+
allowlistHash: z4.string().optional(),
|
|
2465
|
+
/** The install spec the running install was last installed from, so a `source` config change is detected as drift. */
|
|
2466
|
+
installedSource: z4.string().optional(),
|
|
2467
|
+
/** The last fatal error, kept across shutdowns so `headroom status` can explain a daemon that is not running. */
|
|
2468
|
+
lastError: z4.string().optional()
|
|
2469
|
+
});
|
|
2470
|
+
var HeadroomSessionSchema = z4.strictObject({ pid: z4.number().int().positive(), startedAt: z4.number() });
|
|
2471
|
+
var HeadroomLockSchema = z4.strictObject({ pid: z4.number().int().positive(), at: z4.number() });
|
|
2472
|
+
function parseJson(raw, schema) {
|
|
2473
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
2474
|
+
return void 0;
|
|
2475
|
+
}
|
|
2476
|
+
const parsed = schema.safeParse(JSON.parse(raw));
|
|
2477
|
+
return parsed.success ? parsed.data : void 0;
|
|
2478
|
+
}
|
|
2479
|
+
function readHeadroomState(fs2, stateFile) {
|
|
2480
|
+
try {
|
|
2481
|
+
return parseJson(fs2.readFileUtf8(stateFile), HeadroomStateSchema);
|
|
2482
|
+
} catch {
|
|
2483
|
+
return void 0;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
function writeHeadroomState(fs2, stateFile, state) {
|
|
2487
|
+
fs2.mkdirp(path6.dirname(stateFile));
|
|
2488
|
+
fs2.writeFileUtf8(stateFile, `${JSON.stringify(state, null, 2)}
|
|
2489
|
+
`);
|
|
2490
|
+
}
|
|
2491
|
+
function headroomAllowlist(providers) {
|
|
2492
|
+
const urls = /* @__PURE__ */ new Set([HEADROOM_ANTHROPIC_UPSTREAM]);
|
|
2493
|
+
for (const provider of providers) {
|
|
2494
|
+
urls.add(provider.baseUrl);
|
|
2495
|
+
}
|
|
2496
|
+
return [...urls].sort();
|
|
2497
|
+
}
|
|
2498
|
+
function headroomUpstreams(providers, frontDoorOrigin2) {
|
|
2499
|
+
const upstreams = providers.filter((provider) => !isCodexProvider(provider)).map((provider) => ({ baseUrl: provider.baseUrl }));
|
|
2500
|
+
if (providers.length > 0 && frontDoorOrigin2 !== void 0) {
|
|
2501
|
+
upstreams.push({ baseUrl: frontDoorOrigin2 });
|
|
2502
|
+
}
|
|
2503
|
+
return upstreams;
|
|
2504
|
+
}
|
|
2505
|
+
function hashAllowlist(allowlist) {
|
|
2506
|
+
return createHash("sha256").update(allowlist.join("\n")).digest("hex");
|
|
2507
|
+
}
|
|
2508
|
+
function readAllProviders(fs2, providersDir) {
|
|
2509
|
+
const result = [];
|
|
2510
|
+
for (const name of fs2.readdir(providersDir)) {
|
|
2511
|
+
if (!name.endsWith(".json")) {
|
|
2512
|
+
continue;
|
|
2513
|
+
}
|
|
2514
|
+
try {
|
|
2515
|
+
const parsed = parseJson(fs2.readFileUtf8(path6.join(providersDir, name)), ProviderSchema);
|
|
2516
|
+
if (parsed !== void 0) {
|
|
2517
|
+
result.push({ name: name.slice(0, -".json".length), provider: parsed });
|
|
2518
|
+
}
|
|
2519
|
+
} catch {
|
|
2520
|
+
continue;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
return result;
|
|
2524
|
+
}
|
|
2525
|
+
function writeSession(fs2, sessionsDir, session) {
|
|
2526
|
+
fs2.mkdirp(sessionsDir);
|
|
2527
|
+
fs2.writeFileUtf8(path6.join(sessionsDir, `${String(session.pid)}.json`), `${JSON.stringify(session, null, 2)}
|
|
2528
|
+
`);
|
|
2529
|
+
}
|
|
2530
|
+
function removeSession(fs2, sessionsDir, pid) {
|
|
2531
|
+
fs2.removeRecursive(path6.join(sessionsDir, `${String(pid)}.json`));
|
|
2532
|
+
}
|
|
2533
|
+
function listSessions(fs2, sessionsDir) {
|
|
2534
|
+
const sessions = [];
|
|
2535
|
+
for (const name of fs2.readdir(sessionsDir)) {
|
|
2536
|
+
if (!name.endsWith(".json")) {
|
|
2537
|
+
continue;
|
|
2538
|
+
}
|
|
2539
|
+
try {
|
|
2540
|
+
const parsed = parseJson(fs2.readFileUtf8(path6.join(sessionsDir, name)), HeadroomSessionSchema);
|
|
2541
|
+
if (parsed !== void 0) {
|
|
2542
|
+
sessions.push(parsed);
|
|
2543
|
+
}
|
|
2544
|
+
} catch {
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
return sessions.sort((a, b) => a.pid - b.pid);
|
|
2549
|
+
}
|
|
2550
|
+
function pruneDeadSessions(fs2, sessionsDir, isRunning) {
|
|
2551
|
+
const removed = [];
|
|
2552
|
+
for (const session of listSessions(fs2, sessionsDir)) {
|
|
2553
|
+
if (!isRunning(session.pid)) {
|
|
2554
|
+
removeSession(fs2, sessionsDir, session.pid);
|
|
2555
|
+
removed.push(session.pid);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
return removed;
|
|
2559
|
+
}
|
|
2560
|
+
function readStartLock(fs2, lockFile) {
|
|
2561
|
+
try {
|
|
2562
|
+
return parseJson(fs2.readFileUtf8(lockFile), HeadroomLockSchema);
|
|
2563
|
+
} catch {
|
|
2564
|
+
return void 0;
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// src/frontdoor/ensure.ts
|
|
2569
|
+
var FRONTDOOR_START_TIMEOUT_MS = 3e4;
|
|
2570
|
+
var FRONTDOOR_LAUNCHER_POLL_MS = 100;
|
|
2571
|
+
var FrontDoorStartError = class extends CliError {
|
|
2572
|
+
constructor(message) {
|
|
2573
|
+
super(message);
|
|
2574
|
+
this.name = "FrontDoorStartError";
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
function ensureFrontDoor(params) {
|
|
2578
|
+
const { paths, ports } = params;
|
|
2579
|
+
const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
|
|
2580
|
+
let spawned = false;
|
|
2581
|
+
const distrusted = /* @__PURE__ */ new Set();
|
|
2582
|
+
let lastRejection;
|
|
2583
|
+
for (; ; ) {
|
|
2584
|
+
const state = readFrontDoorState(ports.fs, paths.frontdoorStateFile);
|
|
2585
|
+
const supervisorPid = state?.supervisorPid;
|
|
2586
|
+
if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
|
|
2587
|
+
if (state?.port !== void 0 && state.connectPort !== void 0) {
|
|
2588
|
+
const verdict = ports.verifyListener(state.port);
|
|
2589
|
+
if (verdict.ok) {
|
|
2590
|
+
const token = randomUUID2();
|
|
2591
|
+
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
|
2592
|
+
return { port: state.port, connectPort: state.connectPort, token };
|
|
2593
|
+
}
|
|
2594
|
+
lastRejection = `the listener on 127.0.0.1:${String(state.port)} (recorded for pid ${String(supervisorPid)}) failed authentication: ${verdict.reason}`;
|
|
2595
|
+
if (spawned) {
|
|
2596
|
+
throw new FrontDoorStartError(`claude-use: ${lastRejection}; refusing to send this session's credentials to it. Daemon log: ${paths.frontdoorLogPath}`);
|
|
2597
|
+
}
|
|
2598
|
+
distrusted.add(supervisorPid);
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
if (state?.lastError !== void 0) {
|
|
2602
|
+
throw new FrontDoorStartError(`claude-use: the front door reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.frontdoorLogPath})`);
|
|
2603
|
+
}
|
|
2604
|
+
} else if (!spawned) {
|
|
2605
|
+
ports.fs.mkdirp(paths.frontdoorDir);
|
|
2606
|
+
const created = ports.fs.writeFileExclusive(paths.frontdoorLockFile, `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
|
|
2607
|
+
`);
|
|
2608
|
+
if (created) {
|
|
2609
|
+
ports.spawnSupervisor(paths);
|
|
2610
|
+
spawned = true;
|
|
2611
|
+
} else {
|
|
2612
|
+
const lock = readStartLock(ports.fs, paths.frontdoorLockFile);
|
|
2613
|
+
if (lock === void 0 || !ports.isRunning(lock.pid)) {
|
|
2614
|
+
ports.fs.removeRecursive(paths.frontdoorLockFile);
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
if (ports.now() >= deadline) {
|
|
2620
|
+
const lastError = state?.lastError;
|
|
2621
|
+
throw new FrontDoorStartError(
|
|
2622
|
+
`claude-use: the front door did not become ready within ${String(FRONTDOOR_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + (lastRejection === void 0 ? "" : ` (${lastRejection})`) + `. Daemon log: ${paths.frontdoorLogPath}`
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
// src/headroom/ensure.ts
|
|
2630
|
+
var HEADROOM_START_TIMEOUT_MS = 12e4;
|
|
2631
|
+
var HEADROOM_LAUNCHER_POLL_MS = 250;
|
|
2632
|
+
var HeadroomStartError = class extends CliError {
|
|
2633
|
+
constructor(message) {
|
|
2634
|
+
super(message);
|
|
2635
|
+
this.name = "HeadroomStartError";
|
|
2636
|
+
}
|
|
2637
|
+
};
|
|
2638
|
+
function ensureHeadroom(params) {
|
|
2639
|
+
const { paths, ports } = params;
|
|
2640
|
+
const deadline = ports.now() + HEADROOM_START_TIMEOUT_MS;
|
|
2641
|
+
let spawnedSupervisor = false;
|
|
2642
|
+
for (; ; ) {
|
|
2643
|
+
const state = readHeadroomState(ports.fs, paths.headroomStateFile);
|
|
2644
|
+
if (state?.supervisorPid !== void 0 && ports.isRunning(state.supervisorPid)) {
|
|
2645
|
+
const daemonUp = state.port !== void 0 && state.headroomPid !== void 0 && ports.isRunning(state.headroomPid);
|
|
2646
|
+
if (daemonUp && state.port !== void 0) {
|
|
2647
|
+
writeSession(ports.fs, paths.headroomSessionsDir, { pid: params.launcherPid, startedAt: ports.now() });
|
|
2648
|
+
return { port: state.port };
|
|
2649
|
+
}
|
|
2650
|
+
if (state.lastError !== void 0) {
|
|
2651
|
+
throw new HeadroomStartError(
|
|
2652
|
+
`claude-use: the headroom daemon reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.headroomLogPath})`
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
} else if (!spawnedSupervisor) {
|
|
2656
|
+
ports.fs.mkdirp(paths.headroomDir);
|
|
2657
|
+
const created = ports.fs.writeFileExclusive(
|
|
2658
|
+
paths.headroomLockFile,
|
|
2659
|
+
`${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
|
|
2660
|
+
`
|
|
2661
|
+
);
|
|
2662
|
+
if (created) {
|
|
2663
|
+
ports.spawnSupervisor(paths);
|
|
2664
|
+
spawnedSupervisor = true;
|
|
2665
|
+
} else {
|
|
2666
|
+
const lock = readStartLock(ports.fs, paths.headroomLockFile);
|
|
2667
|
+
if (lock === void 0 || !ports.isRunning(lock.pid)) {
|
|
2668
|
+
ports.fs.removeRecursive(paths.headroomLockFile);
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
if (ports.now() >= deadline) {
|
|
2674
|
+
const lastError = state?.lastError;
|
|
2675
|
+
throw new HeadroomStartError(
|
|
2676
|
+
`claude-use: the headroom daemon did not become ready within ${String(HEADROOM_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + `. Daemon log: ${paths.headroomLogPath}`
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
ports.sleep(HEADROOM_LAUNCHER_POLL_MS);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
// src/headroom/supervisor.ts
|
|
2684
|
+
function resolveSupervisorConfig(configured) {
|
|
2685
|
+
return {
|
|
2686
|
+
source: configured.source ?? HEADROOM_DEFAULT_SOURCE,
|
|
2687
|
+
idleShutdownMinutes: configured.idleShutdownMinutes ?? HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
var HEADROOM_BACKOFF_BASE_MS = 500;
|
|
2691
|
+
var HEADROOM_BACKOFF_CAP_MS = 3e4;
|
|
2692
|
+
var HEADROOM_START_RETRY_BUDGET = 5;
|
|
2693
|
+
var HEADROOM_READY_TIMEOUT_MS = 15e3;
|
|
2694
|
+
var HEADROOM_POLL_MS = 1e3;
|
|
2695
|
+
var MS_PER_MINUTE2 = 6e4;
|
|
2696
|
+
function backoffForAttempt(attempt) {
|
|
2697
|
+
return Math.min(HEADROOM_BACKOFF_CAP_MS, HEADROOM_BACKOFF_BASE_MS * 2 ** (attempt - 1));
|
|
2698
|
+
}
|
|
2699
|
+
function versionSatisfies(version, source) {
|
|
2700
|
+
if (version === void 0) {
|
|
2701
|
+
return false;
|
|
2702
|
+
}
|
|
2703
|
+
const specifier = /==|>=|<=|!=|>|<|~=/.exec(source);
|
|
2704
|
+
if (specifier === null) {
|
|
2705
|
+
return true;
|
|
2706
|
+
}
|
|
2707
|
+
const installed = parseVersion(version);
|
|
2708
|
+
const wanted = parseVersion(source.slice(specifier.index + specifier[0].length));
|
|
2709
|
+
if (installed === void 0 || wanted === void 0) {
|
|
2710
|
+
return true;
|
|
2711
|
+
}
|
|
2712
|
+
const cmp = compareVersions(installed, wanted);
|
|
2713
|
+
switch (specifier[0]) {
|
|
2714
|
+
case "==":
|
|
2715
|
+
return cmp === 0;
|
|
2716
|
+
case "!=":
|
|
2717
|
+
return cmp !== 0;
|
|
2718
|
+
case ">=":
|
|
2719
|
+
return cmp >= 0;
|
|
2720
|
+
case "<=":
|
|
2721
|
+
return cmp <= 0;
|
|
2722
|
+
case ">":
|
|
2723
|
+
return cmp > 0;
|
|
2724
|
+
case "<":
|
|
2725
|
+
return cmp < 0;
|
|
2726
|
+
case "~=":
|
|
2727
|
+
return compareVersions(installed.slice(0, Math.max(0, wanted.length - 1)), wanted.slice(0, Math.max(0, wanted.length - 1))) === 0 && cmp >= 0;
|
|
2728
|
+
default:
|
|
2729
|
+
return true;
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
function parseVersion(input) {
|
|
2733
|
+
const match = /(\d+)(\.\d+)*/.exec(input);
|
|
2734
|
+
if (match === null) {
|
|
2735
|
+
return void 0;
|
|
2736
|
+
}
|
|
2737
|
+
return match[0].split(".").map((part) => Number.parseInt(part, 10));
|
|
2738
|
+
}
|
|
2739
|
+
function compareVersions(a, b) {
|
|
2740
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
2741
|
+
const left = a[index] ?? 0;
|
|
2742
|
+
const right = b[index] ?? 0;
|
|
2743
|
+
if (left !== right) {
|
|
2744
|
+
return left - right;
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
return 0;
|
|
2748
|
+
}
|
|
2749
|
+
var HEADROOM_SUPERVISOR_STILL_RUNNING = -1;
|
|
2750
|
+
async function runSupervisor(config, ports, options = {}) {
|
|
2751
|
+
const { fs: fs2, paths } = ports;
|
|
2752
|
+
const previousState = readHeadroomState(fs2, paths.headroomStateFile);
|
|
2753
|
+
let installedSource = previousState?.installedSource;
|
|
2754
|
+
let lastPort = previousState?.lastPort;
|
|
2755
|
+
const sticky = () => lastPort === void 0 ? {} : { lastPort };
|
|
2756
|
+
const fail = (message) => {
|
|
2757
|
+
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2758
|
+
lastError: message,
|
|
2759
|
+
...sticky()
|
|
2760
|
+
});
|
|
2761
|
+
fs2.removeRecursive(paths.headroomLockFile);
|
|
2762
|
+
ports.log(`claude-use headroom supervisor: ${message}`);
|
|
2763
|
+
return 1;
|
|
2764
|
+
};
|
|
2765
|
+
const ensureInstalled = () => {
|
|
2766
|
+
const version2 = ports.headroomVersion();
|
|
2767
|
+
const versionOk = version2 !== void 0 && versionSatisfies(version2, config.source);
|
|
2768
|
+
if (versionOk && (installedSource === void 0 || installedSource === config.source)) {
|
|
2769
|
+
installedSource = config.source;
|
|
2770
|
+
return true;
|
|
2771
|
+
}
|
|
2772
|
+
ports.log(`claude-use headroom supervisor: installing headroom from ${config.source}`);
|
|
2773
|
+
const result = ports.install(config.source);
|
|
2774
|
+
if (!result.ok) {
|
|
2775
|
+
return false;
|
|
2776
|
+
}
|
|
2777
|
+
installedSource = config.source;
|
|
2778
|
+
return true;
|
|
2779
|
+
};
|
|
2780
|
+
if (!ensureInstalled()) {
|
|
2781
|
+
return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
|
|
2782
|
+
}
|
|
2783
|
+
const version = ports.headroomVersion() ?? "unknown";
|
|
2784
|
+
writeHeadroomState(fs2, paths.headroomStateFile, { supervisorPid: ports.ownPid, version, installedSource, ...sticky() });
|
|
2785
|
+
ports.log(`claude-use headroom supervisor ${String(ports.ownPid)}: managing headroom on allowlist [${allowlistOf(ports).join(", ")}]`);
|
|
2786
|
+
const orphanPid = previousState?.headroomPid;
|
|
2787
|
+
if (orphanPid !== void 0 && ports.isRunning(orphanPid)) {
|
|
2788
|
+
ports.log(`claude-use headroom supervisor: stopping daemon pid ${String(orphanPid)} left behind by the previous supervisor`);
|
|
2789
|
+
ports.stopProcess(orphanPid);
|
|
2790
|
+
}
|
|
2791
|
+
let headroomPid;
|
|
2792
|
+
let runningHash;
|
|
2793
|
+
let consecutiveFailures = 0;
|
|
2794
|
+
let idleSince;
|
|
2795
|
+
let ticks = 0;
|
|
2796
|
+
for (; ; ) {
|
|
2797
|
+
if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
|
|
2798
|
+
return HEADROOM_SUPERVISOR_STILL_RUNNING;
|
|
2799
|
+
}
|
|
2800
|
+
ticks += 1;
|
|
2801
|
+
const allowlist = allowlistOf(ports);
|
|
2802
|
+
const allowlistHash = hashAllowlist(allowlist);
|
|
2803
|
+
pruneDeadSessions(fs2, paths.headroomSessionsDir, ports.isRunning);
|
|
2804
|
+
const crashed = headroomPid !== void 0 && !ports.isRunning(headroomPid);
|
|
2805
|
+
if (headroomPid === void 0 || crashed) {
|
|
2806
|
+
if (crashed) {
|
|
2807
|
+
ports.log(`claude-use headroom supervisor: headroom pid ${String(headroomPid)} died`);
|
|
2808
|
+
headroomPid = void 0;
|
|
2809
|
+
runningHash = void 0;
|
|
2810
|
+
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2811
|
+
supervisorPid: ports.ownPid,
|
|
2812
|
+
version,
|
|
2813
|
+
installedSource,
|
|
2814
|
+
...sticky()
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
if (consecutiveFailures >= HEADROOM_START_RETRY_BUDGET) {
|
|
2818
|
+
return fail(`headroom failed to become ready ${String(consecutiveFailures)} times in a row; giving up`);
|
|
2819
|
+
}
|
|
2820
|
+
if (installedSource !== config.source && !ensureInstalled()) {
|
|
2821
|
+
return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
|
|
2822
|
+
}
|
|
2823
|
+
const previousSticky = lastPort;
|
|
2824
|
+
const port = lastPort !== void 0 && await ports.isPortFree(lastPort) ? lastPort : await ports.freePort();
|
|
2825
|
+
const pid = ports.spawnHeadroom(port, allowlist);
|
|
2826
|
+
const ready = await waitUntilReady(ports, port);
|
|
2827
|
+
if (ready) {
|
|
2828
|
+
consecutiveFailures = 0;
|
|
2829
|
+
headroomPid = pid;
|
|
2830
|
+
runningHash = allowlistHash;
|
|
2831
|
+
lastPort = port;
|
|
2832
|
+
if (previousSticky !== void 0 && port !== previousSticky) {
|
|
2833
|
+
ports.log(
|
|
2834
|
+
`claude-use headroom supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(port)} (sessions launched against the old port are stale until they relaunch)`
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2838
|
+
supervisorPid: ports.ownPid,
|
|
2839
|
+
headroomPid: pid,
|
|
2840
|
+
port,
|
|
2841
|
+
version,
|
|
2842
|
+
allowlistHash,
|
|
2843
|
+
installedSource: config.source,
|
|
2844
|
+
...sticky()
|
|
2845
|
+
});
|
|
2846
|
+
fs2.removeRecursive(paths.headroomLockFile);
|
|
2847
|
+
idleSince = void 0;
|
|
2848
|
+
ports.log(`claude-use headroom supervisor: headroom pid ${String(pid)} ready on 127.0.0.1:${String(port)}`);
|
|
2849
|
+
} else {
|
|
2850
|
+
ports.stopProcess(pid);
|
|
2851
|
+
consecutiveFailures += 1;
|
|
2852
|
+
ports.log(
|
|
2853
|
+
`claude-use headroom supervisor: headroom did not become ready on port ${String(port)} (attempt ${String(consecutiveFailures)} of ${String(HEADROOM_START_RETRY_BUDGET)})`
|
|
2854
|
+
);
|
|
2855
|
+
await ports.sleep(backoffForAttempt(consecutiveFailures));
|
|
2856
|
+
continue;
|
|
2857
|
+
}
|
|
2858
|
+
} else {
|
|
2859
|
+
if (runningHash !== allowlistHash || installedSource !== config.source) {
|
|
2860
|
+
if (listSessions(fs2, paths.headroomSessionsDir).length === 0) {
|
|
2861
|
+
ports.log("claude-use headroom supervisor: configuration drifted and no sessions are live; restarting headroom");
|
|
2862
|
+
ports.stopProcess(headroomPid);
|
|
2863
|
+
headroomPid = void 0;
|
|
2864
|
+
runningHash = void 0;
|
|
2865
|
+
continue;
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
const sessions = listSessions(fs2, paths.headroomSessionsDir);
|
|
2869
|
+
if (sessions.length === 0) {
|
|
2870
|
+
idleSince ??= ports.now();
|
|
2871
|
+
const idleMs = ports.now() - idleSince;
|
|
2872
|
+
if (idleMs >= config.idleShutdownMinutes * MS_PER_MINUTE2) {
|
|
2873
|
+
ports.log(
|
|
2874
|
+
`claude-use headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and exiting`
|
|
2875
|
+
);
|
|
2876
|
+
ports.stopProcess(headroomPid);
|
|
2877
|
+
writeHeadroomState(fs2, paths.headroomStateFile, {
|
|
2878
|
+
version,
|
|
2879
|
+
...sticky()
|
|
2880
|
+
});
|
|
2881
|
+
return 0;
|
|
2882
|
+
}
|
|
2883
|
+
} else {
|
|
2884
|
+
idleSince = void 0;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
await ports.sleep(HEADROOM_POLL_MS);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
function currentHeadroomAllowlist(fs2, paths) {
|
|
2891
|
+
const providers = readAllProviders(fs2, paths.providersDir).map((entry) => entry.provider);
|
|
2892
|
+
return headroomAllowlist(headroomUpstreams(providers, frontDoorOrigin(readFrontDoorState(fs2, paths.frontdoorStateFile))));
|
|
2893
|
+
}
|
|
2894
|
+
function allowlistOf(ports) {
|
|
2895
|
+
return currentHeadroomAllowlist(ports.fs, ports.paths);
|
|
2896
|
+
}
|
|
2897
|
+
async function waitUntilReady(ports, port) {
|
|
2898
|
+
const deadline = ports.now() + HEADROOM_READY_TIMEOUT_MS;
|
|
2899
|
+
for (; ; ) {
|
|
2900
|
+
if (await ports.ready(port)) {
|
|
2901
|
+
return true;
|
|
2902
|
+
}
|
|
2903
|
+
if (ports.now() >= deadline) {
|
|
2904
|
+
return false;
|
|
2905
|
+
}
|
|
2906
|
+
await ports.sleep(HEADROOM_POLL_MS);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
// src/launcher/guard.ts
|
|
2911
|
+
var AMBIENT_CREDENTIAL_VARS = [
|
|
2912
|
+
"ANTHROPIC_API_KEY",
|
|
2913
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
2914
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
2915
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
2916
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
2917
|
+
"CLAUDE_CODE_USE_FOUNDRY"
|
|
2918
|
+
];
|
|
2919
|
+
function detectAmbientCredential(env, injected) {
|
|
2920
|
+
for (const variable of AMBIENT_CREDENTIAL_VARS) {
|
|
2921
|
+
const value = env[variable];
|
|
2922
|
+
if (value === void 0 || value === "") {
|
|
2923
|
+
continue;
|
|
2924
|
+
}
|
|
2925
|
+
if (injected?.variable === variable && value === injected.token) {
|
|
2926
|
+
continue;
|
|
2927
|
+
}
|
|
2928
|
+
return { variable };
|
|
2929
|
+
}
|
|
2930
|
+
return void 0;
|
|
2931
|
+
}
|
|
2932
|
+
function formatAmbientCredentialGuardMessage(variable, identityName) {
|
|
2933
|
+
const identitySetCommand = identityName === void 0 ? "claude-use identity set <name> --allow-ambient-credential" : `claude-use identity set ${identityName} --allow-ambient-credential`;
|
|
2934
|
+
return [
|
|
2935
|
+
`error: ${variable} is set in the environment. This identity's isolated`,
|
|
2936
|
+
"credential would be bypassed \u2014 every identity authenticates as this same key",
|
|
2937
|
+
"while it's set. Unset it, or if this is deliberate, opt in per-launch with",
|
|
2938
|
+
"CLAUDE_USE_ALLOW_AMBIENT_CREDENTIAL=1, or persistently for this identity with",
|
|
2939
|
+
`\`${identitySetCommand}\`.`
|
|
2940
|
+
].join("\n");
|
|
2941
|
+
}
|
|
2942
|
+
function evaluateAmbientCredentialGuard(params) {
|
|
2943
|
+
const detected = detectAmbientCredential(params.env, params.injectedCredential);
|
|
2944
|
+
if (detected === void 0) {
|
|
2945
|
+
return { ok: true };
|
|
2946
|
+
}
|
|
2947
|
+
if (params.allowAmbientCredentialOverride || params.allowAmbientCredential || params.providerSelected === true) {
|
|
2948
|
+
return { ok: true };
|
|
2949
|
+
}
|
|
2950
|
+
return {
|
|
2951
|
+
ok: false,
|
|
2952
|
+
variable: detected.variable,
|
|
2953
|
+
message: formatAmbientCredentialGuardMessage(detected.variable, params.identityName)
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
export {
|
|
2957
|
+
AMBIENT_CREDENTIAL_VARS,
|
|
2958
|
+
CONNECT_INTERCEPT_HOST,
|
|
2959
|
+
CONNECT_LIMITS,
|
|
2960
|
+
FARM_MANIFEST_FILENAME,
|
|
2961
|
+
FrontDoorStartError,
|
|
2962
|
+
HeadroomStartError,
|
|
2963
|
+
ROUTED_PATH_PREFIX,
|
|
2964
|
+
buildEntryFacts,
|
|
2965
|
+
carryOver,
|
|
2966
|
+
createLeafCache,
|
|
2967
|
+
detectAmbientCredential,
|
|
2968
|
+
ensureCa,
|
|
2969
|
+
ensureFrontDoor,
|
|
2970
|
+
ensureHeadroom,
|
|
2971
|
+
evaluateAmbientCredentialGuard,
|
|
2972
|
+
formatAmbientCredentialGuardMessage,
|
|
2973
|
+
forwardableHeaders,
|
|
2974
|
+
generateCa,
|
|
2975
|
+
isInterceptedHost,
|
|
2976
|
+
mintLeaf,
|
|
2977
|
+
parseConnectTarget,
|
|
2978
|
+
readFarmManifest,
|
|
2979
|
+
realConnectCertStore,
|
|
2980
|
+
realConnectEffects,
|
|
2981
|
+
recoverFarm,
|
|
2982
|
+
recoveryDiagnostics,
|
|
2983
|
+
resolveDecisions,
|
|
2984
|
+
resolveSupervisorConfig,
|
|
2985
|
+
resyncFarm,
|
|
2986
|
+
runFrontDoorSupervisor,
|
|
2987
|
+
runSupervisor,
|
|
2988
|
+
servedByPipeline,
|
|
2989
|
+
startConnectServer,
|
|
2990
|
+
topLevelNames
|
|
2991
|
+
};
|