agent-shim 0.0.0 → 7.1.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/LICENSE +201 -0
- package/README.md +233 -0
- package/dist/cli.cjs +69549 -0
- package/dist/index.cjs +3620 -0
- package/dist/index.mjs +3545 -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 +746 -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/frontdoor/x509.d.ts +11 -0
- package/dist/types/headroom/ensure.d.ts +31 -0
- package/dist/types/headroom/settings.d.ts +13 -0
- package/dist/types/headroom/source.d.ts +8 -0
- package/dist/types/headroom/state.d.ts +77 -0
- package/dist/types/headroom/supervisor.d.ts +107 -0
- package/dist/types/index.d.ts +21 -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 +135 -0
- package/dist/types/legacy.d.ts +24 -0
- package/dist/types/paths.d.ts +79 -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/dist/types/usage/account.d.ts +20 -0
- package/dist/types/usage/read.d.ts +70 -0
- package/dist/types/usage/schema.d.ts +318 -0
- package/package.json +91 -2
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3545 @@
|
|
|
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.filter((category) => !all || category !== "runtime").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
|
+
trackUsage: z.boolean().optional()
|
|
51
|
+
});
|
|
52
|
+
var CREDENTIAL_TARGETS = ["bearer", "apiKey", "oauthToken"];
|
|
53
|
+
var PROVIDER_CREDENTIAL_TARGETS = ["bearer", "apiKey"];
|
|
54
|
+
var CREDENTIAL_TARGET_VARS = {
|
|
55
|
+
bearer: "ANTHROPIC_AUTH_TOKEN",
|
|
56
|
+
apiKey: "ANTHROPIC_API_KEY",
|
|
57
|
+
oauthToken: "CLAUDE_CODE_OAUTH_TOKEN"
|
|
58
|
+
};
|
|
59
|
+
var ArgvSchema = z.tuple([z.string().min(1)], z.string());
|
|
60
|
+
var CommandSourceOptions = {
|
|
61
|
+
/** 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`. */
|
|
62
|
+
interactive: z.boolean().optional(),
|
|
63
|
+
/** 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. */
|
|
64
|
+
timeoutMs: z.int().positive().optional()
|
|
65
|
+
};
|
|
66
|
+
var CredentialSourceSchema = z.union([
|
|
67
|
+
z.strictObject({ env: z.string().min(1) }),
|
|
68
|
+
z.strictObject({
|
|
69
|
+
file: z.string().min(1).refine((filePath) => filePath.startsWith("~/") || path.isAbsolute(filePath), {
|
|
70
|
+
message: "a credential file must be an absolute or ~-rooted path, since a launch can start in any directory"
|
|
71
|
+
})
|
|
72
|
+
}),
|
|
73
|
+
z.strictObject({ command: ArgvSchema, ...CommandSourceOptions }),
|
|
74
|
+
z.strictObject({ op: z.string().startsWith("op://"), ...CommandSourceOptions }),
|
|
75
|
+
z.strictObject({
|
|
76
|
+
keychain: z.strictObject({ service: z.string().min(1), account: z.string().min(1).optional() }),
|
|
77
|
+
...CommandSourceOptions
|
|
78
|
+
}),
|
|
79
|
+
z.strictObject({ literal: z.string().min(1) })
|
|
80
|
+
]);
|
|
81
|
+
var CREDENTIAL_CACHE_STORES = ["keychain", "file"];
|
|
82
|
+
var CredentialCacheSchema = z.strictObject({
|
|
83
|
+
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(),
|
|
84
|
+
store: z.enum(CREDENTIAL_CACHE_STORES).optional()
|
|
85
|
+
});
|
|
86
|
+
var CredentialSchema = z.strictObject({
|
|
87
|
+
sources: z.array(CredentialSourceSchema).min(1),
|
|
88
|
+
target: z.enum(CREDENTIAL_TARGETS).optional(),
|
|
89
|
+
cache: CredentialCacheSchema.optional()
|
|
90
|
+
});
|
|
91
|
+
var ProviderCredentialSchema = CredentialSchema.extend({ target: z.enum(PROVIDER_CREDENTIAL_TARGETS).optional() });
|
|
92
|
+
var RESERVED_PROVIDER_ENV_KEYS = Object.values(CREDENTIAL_TARGET_VARS);
|
|
93
|
+
var ProviderEnvSchema = z.record(z.string().min(1), z.string()).refine((env) => !Object.keys(env).some((key) => RESERVED_PROVIDER_ENV_KEYS.includes(key)), {
|
|
94
|
+
message: `env may not set ${RESERVED_PROVIDER_ENV_KEYS.join(", ")}: the credential block's target sets one and agent-shim removes the others`
|
|
95
|
+
});
|
|
96
|
+
var CODEX_EFFORTS = ["none", "low", "medium", "high"];
|
|
97
|
+
var CODEX_DEFAULT_TIER_MODELS = Object.freeze({
|
|
98
|
+
fable: "gpt-5.6-sol",
|
|
99
|
+
opus: "gpt-5.6-sol",
|
|
100
|
+
sonnet: "gpt-5.6-terra",
|
|
101
|
+
haiku: "gpt-5.6-luna"
|
|
102
|
+
});
|
|
103
|
+
var CodexProviderConfigSchema = z.strictObject({
|
|
104
|
+
/** The codex model a request maps to when no tier matches its model name. */
|
|
105
|
+
defaultModel: z.string().min(1).optional(),
|
|
106
|
+
/** Per-tier overrides of the shipped mapping, matched by substring of the requested model name in `CODEX_TIERS` order. */
|
|
107
|
+
models: z.strictObject({
|
|
108
|
+
fable: z.string().min(1).optional(),
|
|
109
|
+
opus: z.string().min(1).optional(),
|
|
110
|
+
sonnet: z.string().min(1).optional(),
|
|
111
|
+
haiku: z.string().min(1).optional()
|
|
112
|
+
}).optional(),
|
|
113
|
+
/** The reasoning effort used when the request's own `output_config.effort` is not one the backend accepts. */
|
|
114
|
+
effort: z.enum(CODEX_EFFORTS).optional()
|
|
115
|
+
});
|
|
116
|
+
var HttpProviderSchema = z.strictObject({
|
|
117
|
+
$schema: z.string().optional(),
|
|
118
|
+
kind: z.literal("http").optional(),
|
|
119
|
+
displayName: z.string().min(1),
|
|
120
|
+
baseUrl: z.url(),
|
|
121
|
+
credential: ProviderCredentialSchema,
|
|
122
|
+
env: ProviderEnvSchema.optional()
|
|
123
|
+
});
|
|
124
|
+
var CodexProviderSchema = z.strictObject({
|
|
125
|
+
$schema: z.string().optional(),
|
|
126
|
+
kind: z.literal("codex"),
|
|
127
|
+
displayName: z.string().min(1),
|
|
128
|
+
credential: ProviderCredentialSchema,
|
|
129
|
+
env: ProviderEnvSchema.optional(),
|
|
130
|
+
codex: CodexProviderConfigSchema.optional()
|
|
131
|
+
});
|
|
132
|
+
var ProviderSchema = z.discriminatedUnion("kind", [HttpProviderSchema, CodexProviderSchema]);
|
|
133
|
+
function isCodexProvider(provider) {
|
|
134
|
+
return provider.kind === "codex";
|
|
135
|
+
}
|
|
136
|
+
var ConfigProfileSchema = z.strictObject({
|
|
137
|
+
$schema: z.string().optional(),
|
|
138
|
+
description: z.string().optional(),
|
|
139
|
+
extends: z.array(z.string().min(1)).optional(),
|
|
140
|
+
categories: CategoryMapSchema.optional(),
|
|
141
|
+
entries: EntriesSchema.optional(),
|
|
142
|
+
launch: LaunchSchema.optional()
|
|
143
|
+
});
|
|
144
|
+
var POOL_SELECTOR_PREFIX = "pool:";
|
|
145
|
+
var POOL_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._-]*";
|
|
146
|
+
var IDENTITY_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._@-]*";
|
|
147
|
+
var PoolNameSchema = z.string().min(1).regex(new RegExp(`^${POOL_NAME_PATTERN}$`));
|
|
148
|
+
var IdentitySelectorSchema = z.string().min(1).regex(new RegExp(`^(${POOL_SELECTOR_PREFIX}${POOL_NAME_PATTERN}|${IDENTITY_NAME_PATTERN})$`));
|
|
149
|
+
var PoolSchema = z.strictObject({
|
|
150
|
+
identities: z.array(z.string().min(1).regex(new RegExp(`^${IDENTITY_NAME_PATTERN}$`))).min(1).refine((names) => new Set(names).size === names.length, { message: "pool members must be unique" })
|
|
151
|
+
});
|
|
152
|
+
var DirectoryRuleSchema = ConfigProfileSchema.omit({ description: true }).extend({
|
|
153
|
+
path: z.string().min(1),
|
|
154
|
+
configProfile: z.string().min(1).optional(),
|
|
155
|
+
identity: IdentitySelectorSchema.optional(),
|
|
156
|
+
when: WhenSchema.optional()
|
|
157
|
+
});
|
|
158
|
+
var DirectoryRulesSchema = z.strictObject({
|
|
159
|
+
$schema: z.string().optional(),
|
|
160
|
+
rules: z.array(DirectoryRuleSchema)
|
|
161
|
+
});
|
|
162
|
+
var PortableConfigSchema = ConfigProfileSchema.omit({ description: true }).extend({
|
|
163
|
+
configProfile: z.string().min(1).optional(),
|
|
164
|
+
identity: IdentitySelectorSchema.optional(),
|
|
165
|
+
when: WhenSchema.optional()
|
|
166
|
+
});
|
|
167
|
+
var HEADROOM_MODES = ["cache", "token"];
|
|
168
|
+
var HEADROOM_CCR_MODES = ["default", "lossless", "none"];
|
|
169
|
+
var HEADROOM_ROLLOUT_CHANNELS = ["beta", "canary", "dev"];
|
|
170
|
+
var INTERCEPT_CHANNELS = ["canary", "dev"];
|
|
171
|
+
var READ_MATURATION_CHANNELS = ["beta", "dev"];
|
|
172
|
+
var HeadroomGlobalConfigObjectSchema = z.strictObject({
|
|
173
|
+
/** Install spec handed to `uv tool install`. Defaults to HEADROOM_DEFAULT_SOURCE. */
|
|
174
|
+
source: z.string().min(1).optional(),
|
|
175
|
+
/** How long the daemon may sit with no registered sessions before the supervisor stops it. Defaults to HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES. */
|
|
176
|
+
idleShutdownMinutes: z.number().int().positive().optional(),
|
|
177
|
+
/** Optimisation mode, passed as `--mode`. Unset leaves headroom's own default (`cache`). */
|
|
178
|
+
mode: z.enum(HEADROOM_MODES).optional(),
|
|
179
|
+
/** Keep-ratio for prose and code compression, passed as `--target-ratio`: lower is more aggressive. Unset lets headroom decide. */
|
|
180
|
+
targetRatio: z.number().gt(0).max(1).optional(),
|
|
181
|
+
/** How compressed tool output stays recoverable. Unset leaves headroom's own default (`default`). */
|
|
182
|
+
ccr: z.enum(HEADROOM_CCR_MODES).optional(),
|
|
183
|
+
/** Headroom's experimental release channel, exported as `HEADROOM_ROLLOUT_CHANNEL`. Required by `interceptToolResults` and `readMaturation`, which are refused without a channel that unlocks them. */
|
|
184
|
+
rolloutChannel: z.enum(HEADROOM_ROLLOUT_CHANNELS).optional(),
|
|
185
|
+
/** Opt in to headroom's tool-result interceptors (`--intercept-tool-results`). Needs `rolloutChannel` canary or dev. */
|
|
186
|
+
interceptToolResults: z.boolean().optional(),
|
|
187
|
+
/** Opt in to headroom's experimental read maturation (`--read-maturation`). Needs `rolloutChannel` beta or dev. */
|
|
188
|
+
readMaturation: z.boolean().optional()
|
|
189
|
+
});
|
|
190
|
+
var HeadroomGlobalConfigSchema = HeadroomGlobalConfigObjectSchema.superRefine((config, context) => {
|
|
191
|
+
if (config.interceptToolResults === true && !INTERCEPT_CHANNELS.includes(config.rolloutChannel ?? "")) {
|
|
192
|
+
context.addIssue({ code: "custom", path: ["interceptToolResults"], message: "interceptToolResults needs rolloutChannel canary or dev" });
|
|
193
|
+
}
|
|
194
|
+
if (config.readMaturation === true && !READ_MATURATION_CHANNELS.includes(config.rolloutChannel ?? "")) {
|
|
195
|
+
context.addIssue({ code: "custom", path: ["readMaturation"], message: "readMaturation needs rolloutChannel beta or dev" });
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
var HEADROOM_DEFAULT_SOURCE = "headroom-ai[proxy] @ git+https://github.com/ExaDev/headroom@ecd1c351fcfed97499721e86852e148436ff2ee6";
|
|
199
|
+
var HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES = 15;
|
|
200
|
+
var FrontDoorGlobalConfigSchema = z.strictObject({
|
|
201
|
+
/** How long the front door may sit with no registered sessions before it closes and exits. Defaults to FRONTDOOR_DEFAULT_IDLE_SHUTDOWN_MINUTES. */
|
|
202
|
+
idleShutdownMinutes: z.number().int().positive().optional()
|
|
203
|
+
});
|
|
204
|
+
var GlobalConfigSchema = z.strictObject({
|
|
205
|
+
$schema: z.string().optional(),
|
|
206
|
+
defaultConfigProfile: z.string().min(1).optional(),
|
|
207
|
+
walkUpLimit: z.string().min(1).optional(),
|
|
208
|
+
categories: CategoryMapSchema.optional(),
|
|
209
|
+
entries: EntriesSchema.optional(),
|
|
210
|
+
launch: LaunchSchema.optional(),
|
|
211
|
+
headroom: HeadroomGlobalConfigSchema.optional(),
|
|
212
|
+
frontdoor: FrontDoorGlobalConfigSchema.optional(),
|
|
213
|
+
pools: z.record(PoolNameSchema, PoolSchema).optional()
|
|
214
|
+
});
|
|
215
|
+
var IdentitySchema = z.strictObject({
|
|
216
|
+
$schema: z.string().optional(),
|
|
217
|
+
name: z.string().min(1).regex(/^[A-Za-z0-9][A-Za-z0-9._@-]*$/),
|
|
218
|
+
defaultConfigProfile: z.string().min(1).optional(),
|
|
219
|
+
allowAmbientCredential: z.boolean().default(false),
|
|
220
|
+
credential: CredentialSchema.optional()
|
|
221
|
+
});
|
|
222
|
+
var CategoryClassificationSchema = z.strictObject({
|
|
223
|
+
$schema: z.string().optional(),
|
|
224
|
+
secret: z.array(z.string().min(1)),
|
|
225
|
+
runtime: z.array(z.string().min(1)),
|
|
226
|
+
history: z.array(z.string().min(1)),
|
|
227
|
+
knowledge: z.array(z.string().min(1)),
|
|
228
|
+
settings: z.array(z.string().min(1))
|
|
229
|
+
});
|
|
230
|
+
var CategoryClassificationOverlaySchema = z.strictObject({
|
|
231
|
+
$schema: z.string().optional(),
|
|
232
|
+
secret: z.array(z.string().min(1)).optional(),
|
|
233
|
+
runtime: z.array(z.string().min(1)).optional(),
|
|
234
|
+
history: z.array(z.string().min(1)).optional(),
|
|
235
|
+
knowledge: z.array(z.string().min(1)).optional(),
|
|
236
|
+
settings: z.array(z.string().min(1)).optional()
|
|
237
|
+
});
|
|
238
|
+
var SHIPPED_CATEGORY_DEFAULTS = Object.freeze({
|
|
239
|
+
secret: false,
|
|
240
|
+
runtime: false,
|
|
241
|
+
history: true,
|
|
242
|
+
knowledge: true,
|
|
243
|
+
settings: true
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// src/config/load.ts
|
|
247
|
+
import { cosmiconfigSync } from "cosmiconfig";
|
|
248
|
+
|
|
249
|
+
// src/cliError.ts
|
|
250
|
+
var EXIT_FAILURE = 1;
|
|
251
|
+
var CliError = class extends Error {
|
|
252
|
+
/** The process exit status this failure maps to. `EXIT_FAILURE` unless a subclass says otherwise. */
|
|
253
|
+
exitCode = EXIT_FAILURE;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// src/config/classify.ts
|
|
257
|
+
var GLOB_METACHARACTERS = /[*?[\]{}!()]/;
|
|
258
|
+
function isExactPattern(pattern) {
|
|
259
|
+
return !GLOB_METACHARACTERS.test(pattern);
|
|
260
|
+
}
|
|
261
|
+
function compile(pattern, category, source, ordinal) {
|
|
262
|
+
const isExact = isExactPattern(pattern);
|
|
263
|
+
const matcher = picomatch(pattern, { dot: true, nocase: false });
|
|
264
|
+
return {
|
|
265
|
+
pattern,
|
|
266
|
+
category,
|
|
267
|
+
source,
|
|
268
|
+
isExact,
|
|
269
|
+
ordinal,
|
|
270
|
+
matches: isExact ? (name) => name === pattern : (name) => matcher(name)
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function compileClassificationPatterns(defaults, overlay) {
|
|
274
|
+
const compiled = [];
|
|
275
|
+
let ordinal = 0;
|
|
276
|
+
for (const category of CATEGORY_NAMES) {
|
|
277
|
+
for (const pattern of defaults[category]) {
|
|
278
|
+
compiled.push(compile(pattern, category, "default", ordinal));
|
|
279
|
+
ordinal += 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (overlay !== void 0) {
|
|
283
|
+
for (const category of CATEGORY_NAMES) {
|
|
284
|
+
for (const pattern of overlay[category] ?? []) {
|
|
285
|
+
compiled.push(compile(pattern, category, "local", ordinal));
|
|
286
|
+
ordinal += 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return compiled;
|
|
291
|
+
}
|
|
292
|
+
function compareClassificationPatterns(a, b) {
|
|
293
|
+
if (a.isExact !== b.isExact) {
|
|
294
|
+
return a.isExact ? 1 : -1;
|
|
295
|
+
}
|
|
296
|
+
if (a.source !== b.source) {
|
|
297
|
+
return a.source === "local" ? 1 : -1;
|
|
298
|
+
}
|
|
299
|
+
if (a.pattern.length !== b.pattern.length) {
|
|
300
|
+
return a.pattern.length - b.pattern.length;
|
|
301
|
+
}
|
|
302
|
+
return a.ordinal - b.ordinal;
|
|
303
|
+
}
|
|
304
|
+
function classifyEntries(names, options) {
|
|
305
|
+
const patterns = compileClassificationPatterns(options.defaults, options.overlay);
|
|
306
|
+
const classification = /* @__PURE__ */ new Map();
|
|
307
|
+
const decidedBy = /* @__PURE__ */ new Map();
|
|
308
|
+
const unclassified = [];
|
|
309
|
+
for (const name of names) {
|
|
310
|
+
let winner;
|
|
311
|
+
for (const candidate of patterns) {
|
|
312
|
+
if (!candidate.matches(name)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (winner === void 0 || compareClassificationPatterns(candidate, winner) > 0) {
|
|
316
|
+
winner = candidate;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (winner === void 0) {
|
|
320
|
+
classification.set(name, null);
|
|
321
|
+
unclassified.push(name);
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
classification.set(name, winner.category);
|
|
325
|
+
decidedBy.set(name, winner);
|
|
326
|
+
}
|
|
327
|
+
return { classification, unclassified, decidedBy };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/resolve/conditions.ts
|
|
331
|
+
import picomatch2 from "picomatch";
|
|
332
|
+
var MILLISECONDS_PER_UNIT = Object.freeze({
|
|
333
|
+
ms: 1,
|
|
334
|
+
s: 1e3,
|
|
335
|
+
m: 6e4,
|
|
336
|
+
h: 36e5,
|
|
337
|
+
d: 864e5,
|
|
338
|
+
w: 6048e5
|
|
339
|
+
});
|
|
340
|
+
var DURATION_PARTS_RE = /^(0|[1-9][0-9]*)(ms|s|m|h|d|w)$/;
|
|
341
|
+
function parseDuration(value) {
|
|
342
|
+
const parts = DURATION_PARTS_RE.exec(value);
|
|
343
|
+
if (parts === null) {
|
|
344
|
+
throw new Error(`"${value}" is not a valid duration. Expected a count followed by ms, s, m, h, d, or w.`);
|
|
345
|
+
}
|
|
346
|
+
const [, count, unit] = parts;
|
|
347
|
+
if (count === void 0 || unit === void 0) {
|
|
348
|
+
throw new Error(`"${value}" is not a valid duration.`);
|
|
349
|
+
}
|
|
350
|
+
const multiplier = MILLISECONDS_PER_UNIT[unit];
|
|
351
|
+
if (multiplier === void 0) {
|
|
352
|
+
throw new Error(`"${value}" is not a valid duration.`);
|
|
353
|
+
}
|
|
354
|
+
return Number(count) * multiplier;
|
|
355
|
+
}
|
|
356
|
+
function matchBranch(pattern, branch, detached = false) {
|
|
357
|
+
if (branch === void 0 || branch === "" || detached) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
return picomatch2(pattern, { dot: true, nocase: false })(branch);
|
|
361
|
+
}
|
|
362
|
+
function evaluateWhen(when, context) {
|
|
363
|
+
if (when === void 0) {
|
|
364
|
+
return { passed: true, checked: [], failed: [] };
|
|
365
|
+
}
|
|
366
|
+
const checked = [];
|
|
367
|
+
const failed = [];
|
|
368
|
+
if (when.newerThan !== void 0) {
|
|
369
|
+
checked.push("newerThan");
|
|
370
|
+
const window = parseDuration(when.newerThan);
|
|
371
|
+
const latest = context.fact?.latestMtimeMs;
|
|
372
|
+
if (latest === void 0 || context.nowMs - latest > window) {
|
|
373
|
+
failed.push("newerThan");
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (when.olderThan !== void 0) {
|
|
377
|
+
checked.push("olderThan");
|
|
378
|
+
const window = parseDuration(when.olderThan);
|
|
379
|
+
const latest = context.fact?.latestMtimeMs;
|
|
380
|
+
if (latest === void 0 || context.nowMs - latest <= window) {
|
|
381
|
+
failed.push("olderThan");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (when.maxSizeBytes !== void 0) {
|
|
385
|
+
checked.push("maxSizeBytes");
|
|
386
|
+
const total = context.fact?.totalSizeBytes;
|
|
387
|
+
if (total === void 0 || total > when.maxSizeBytes) {
|
|
388
|
+
failed.push("maxSizeBytes");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (when.branch !== void 0) {
|
|
392
|
+
checked.push("branch");
|
|
393
|
+
if (!matchBranch(when.branch, context.branch, context.branchDetached ?? false)) {
|
|
394
|
+
failed.push("branch");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (when.env !== void 0) {
|
|
398
|
+
checked.push("env");
|
|
399
|
+
const mismatch = Object.entries(when.env).some(([name, expected]) => context.env[name] !== expected);
|
|
400
|
+
if (mismatch) {
|
|
401
|
+
failed.push("env");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return { passed: failed.length === 0, checked, failed };
|
|
405
|
+
}
|
|
406
|
+
function isVacuousWhen(when) {
|
|
407
|
+
return when !== void 0 && Object.keys(when).length === 0;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/resolve/match.ts
|
|
411
|
+
import picomatch3 from "picomatch";
|
|
412
|
+
|
|
413
|
+
// src/resolve/projects.ts
|
|
414
|
+
var NON_ALPHANUMERIC = /[^A-Za-z0-9]/g;
|
|
415
|
+
var WILDCARD_TOKENS = ["**", "*", "?"];
|
|
416
|
+
function encodeProjectPath(realPath) {
|
|
417
|
+
return realPath.replace(NON_ALPHANUMERIC, "-");
|
|
418
|
+
}
|
|
419
|
+
function splitOnWildcards(pattern) {
|
|
420
|
+
const fragments = [];
|
|
421
|
+
let literal = "";
|
|
422
|
+
let index = 0;
|
|
423
|
+
while (index < pattern.length) {
|
|
424
|
+
const token = WILDCARD_TOKENS.find((candidate) => pattern.startsWith(candidate, index));
|
|
425
|
+
if (token === void 0) {
|
|
426
|
+
literal += pattern.charAt(index);
|
|
427
|
+
index += 1;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (literal !== "") {
|
|
431
|
+
fragments.push({ kind: "literal", text: literal });
|
|
432
|
+
literal = "";
|
|
433
|
+
}
|
|
434
|
+
fragments.push({ kind: "wildcard", text: token });
|
|
435
|
+
index += token.length;
|
|
436
|
+
}
|
|
437
|
+
if (literal !== "") {
|
|
438
|
+
fragments.push({ kind: "literal", text: literal });
|
|
439
|
+
}
|
|
440
|
+
return fragments;
|
|
441
|
+
}
|
|
442
|
+
var UnrootedProjectPathError = class extends CliError {
|
|
443
|
+
constructor(fragment) {
|
|
444
|
+
super(
|
|
445
|
+
`"${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.`
|
|
446
|
+
);
|
|
447
|
+
this.fragment = fragment;
|
|
448
|
+
this.name = "UnrootedProjectPathError";
|
|
449
|
+
}
|
|
450
|
+
fragment;
|
|
451
|
+
};
|
|
452
|
+
function expandHome(fragment, home) {
|
|
453
|
+
if (fragment === "~") {
|
|
454
|
+
return home;
|
|
455
|
+
}
|
|
456
|
+
if (fragment.startsWith("~/")) {
|
|
457
|
+
return home + fragment.slice(1);
|
|
458
|
+
}
|
|
459
|
+
return fragment;
|
|
460
|
+
}
|
|
461
|
+
function encodeProjectPattern(fragment, options) {
|
|
462
|
+
const expanded = expandHome(fragment, options.home);
|
|
463
|
+
if (!expanded.startsWith("/")) {
|
|
464
|
+
throw new UnrootedProjectPathError(fragment);
|
|
465
|
+
}
|
|
466
|
+
return splitOnWildcards(expanded).map((piece) => piece.kind === "literal" ? encodeProjectPath(piece.text) : piece.text).join("");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// src/resolve/match.ts
|
|
470
|
+
var GLOB_METACHARACTERS2 = /[*?[\]{}!()]/;
|
|
471
|
+
function isExactPattern2(pattern) {
|
|
472
|
+
return !GLOB_METACHARACTERS2.test(pattern);
|
|
473
|
+
}
|
|
474
|
+
function literalPrefixOf(pattern) {
|
|
475
|
+
const match = GLOB_METACHARACTERS2.exec(pattern);
|
|
476
|
+
return match === null ? pattern : pattern.slice(0, match.index);
|
|
477
|
+
}
|
|
478
|
+
function normaliseRelative(fragment) {
|
|
479
|
+
const segments = fragment.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
480
|
+
return segments.join("/");
|
|
481
|
+
}
|
|
482
|
+
var PROJECTS_PREFIX = "projects/";
|
|
483
|
+
var EntryKeyError = class extends CliError {
|
|
484
|
+
constructor(key, message, reason) {
|
|
485
|
+
super(message);
|
|
486
|
+
this.key = key;
|
|
487
|
+
this.reason = reason;
|
|
488
|
+
this.name = "EntryKeyError";
|
|
489
|
+
}
|
|
490
|
+
key;
|
|
491
|
+
reason;
|
|
492
|
+
};
|
|
493
|
+
function canonicaliseEntryKey(key, options) {
|
|
494
|
+
const separator = key.indexOf("/");
|
|
495
|
+
if (separator <= 0) {
|
|
496
|
+
throw new EntryKeyError(key, `Entry key "${key}" has no "<category>/" prefix.`, "malformed");
|
|
497
|
+
}
|
|
498
|
+
const category = key.slice(0, separator);
|
|
499
|
+
if (!isCategoryName(category)) {
|
|
500
|
+
throw new EntryKeyError(key, `Entry key "${key}" starts with "${category}", which is not a category name.`, "malformed");
|
|
501
|
+
}
|
|
502
|
+
const rest = key.slice(separator + 1);
|
|
503
|
+
if (rest === "") {
|
|
504
|
+
throw new EntryKeyError(key, `Entry key "${key}" names a category with no path after it.`, "malformed");
|
|
505
|
+
}
|
|
506
|
+
if (category === "history" && rest.startsWith(PROJECTS_PREFIX)) {
|
|
507
|
+
const fragment = rest.slice(PROJECTS_PREFIX.length);
|
|
508
|
+
try {
|
|
509
|
+
const encoded = encodeProjectPattern(fragment, { home: options.home });
|
|
510
|
+
return { declaredCategory: category, canonicalPattern: PROJECTS_PREFIX + encoded, projectFragment: fragment };
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if (error instanceof UnrootedProjectPathError) {
|
|
513
|
+
throw new EntryKeyError(key, error.message, "unrooted-project-path");
|
|
514
|
+
}
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return { declaredCategory: category, canonicalPattern: normaliseRelative(rest) };
|
|
519
|
+
}
|
|
520
|
+
function compileMatcher(canonicalPattern) {
|
|
521
|
+
const options = { dot: true, nocase: false };
|
|
522
|
+
const self = picomatch3(canonicalPattern, options);
|
|
523
|
+
const subtree = picomatch3(`${canonicalPattern}/**`, options);
|
|
524
|
+
return (relPath) => self(relPath) || subtree(relPath);
|
|
525
|
+
}
|
|
526
|
+
function patternCouldReachUnder(canonicalPattern, dir) {
|
|
527
|
+
const patternSegments = canonicalPattern.split("/").filter((segment) => segment !== "");
|
|
528
|
+
const dirSegments = dir.split("/").filter((segment) => segment !== "");
|
|
529
|
+
return couldReach(patternSegments, dirSegments);
|
|
530
|
+
}
|
|
531
|
+
function couldReach(patternSegments, dirSegments) {
|
|
532
|
+
if (dirSegments.length === 0) {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
if (patternSegments.length === 0) {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
const [head, ...restPattern] = patternSegments;
|
|
539
|
+
if (head === "**") {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
const [dirHead, ...restDir] = dirSegments;
|
|
543
|
+
if (head === void 0 || dirHead === void 0) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
if (!picomatch3(head, { dot: true, nocase: false })(dirHead)) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
return couldReach(restPattern, restDir);
|
|
550
|
+
}
|
|
551
|
+
function compareSpecificity(a, b) {
|
|
552
|
+
if (a.layer !== b.layer) {
|
|
553
|
+
return a.layer - b.layer;
|
|
554
|
+
}
|
|
555
|
+
if (a.isExact !== b.isExact) {
|
|
556
|
+
return a.isExact ? 1 : -1;
|
|
557
|
+
}
|
|
558
|
+
if (a.literalPrefix.length !== b.literalPrefix.length) {
|
|
559
|
+
return a.literalPrefix.length - b.literalPrefix.length;
|
|
560
|
+
}
|
|
561
|
+
if (a.segmentCount !== b.segmentCount) {
|
|
562
|
+
return a.segmentCount - b.segmentCount;
|
|
563
|
+
}
|
|
564
|
+
if (a.ordinal !== b.ordinal) {
|
|
565
|
+
return a.ordinal - b.ordinal;
|
|
566
|
+
}
|
|
567
|
+
if (a.canonicalPattern === b.canonicalPattern) {
|
|
568
|
+
return 0;
|
|
569
|
+
}
|
|
570
|
+
return a.canonicalPattern < b.canonicalPattern ? -1 : 1;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/resolve/flatten.ts
|
|
574
|
+
function unpackEntryValue(value) {
|
|
575
|
+
if (typeof value === "boolean") {
|
|
576
|
+
return { value };
|
|
577
|
+
}
|
|
578
|
+
return { value: value.value, when: value.when };
|
|
579
|
+
}
|
|
580
|
+
function flattenLayers(layers, options) {
|
|
581
|
+
const categories = /* @__PURE__ */ new Map();
|
|
582
|
+
const rules = /* @__PURE__ */ new Map();
|
|
583
|
+
const launch = {};
|
|
584
|
+
const diagnostics = [];
|
|
585
|
+
for (const layer of layers) {
|
|
586
|
+
if (layer.categories !== void 0) {
|
|
587
|
+
for (const [name, value] of Object.entries(layer.categories)) {
|
|
588
|
+
if (!isOverridableCategory(name)) {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
categories.set(name, value);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (layer.launch !== void 0) {
|
|
595
|
+
if (layer.launch.skipPermissions !== void 0) {
|
|
596
|
+
launch.skipPermissions = layer.launch.skipPermissions;
|
|
597
|
+
}
|
|
598
|
+
if (layer.launch.remoteControl !== void 0) {
|
|
599
|
+
launch.remoteControl = layer.launch.remoteControl;
|
|
600
|
+
}
|
|
601
|
+
if (layer.launch.provider !== void 0) {
|
|
602
|
+
launch.provider = layer.launch.provider;
|
|
603
|
+
}
|
|
604
|
+
if (layer.launch.headroom !== void 0) {
|
|
605
|
+
launch.headroom = layer.launch.headroom;
|
|
606
|
+
}
|
|
607
|
+
if (layer.launch.trackUsage !== void 0) {
|
|
608
|
+
launch.trackUsage = layer.launch.trackUsage;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (layer.entries === void 0) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const order = orderedKeys(layer);
|
|
615
|
+
for (const [ordinal, rawKey] of order.entries()) {
|
|
616
|
+
const value = layer.entries[rawKey];
|
|
617
|
+
if (value === void 0) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
let canonical;
|
|
621
|
+
try {
|
|
622
|
+
canonical = canonicaliseEntryKey(rawKey, { home: options.home });
|
|
623
|
+
} catch (error) {
|
|
624
|
+
if (error instanceof EntryKeyError) {
|
|
625
|
+
diagnostics.push({
|
|
626
|
+
code: error.reason === "unrooted-project-path" ? "UNROOTED_PROJECT_PATH" : "MALFORMED_ENTRY_KEY",
|
|
627
|
+
severity: "error",
|
|
628
|
+
message: error.message,
|
|
629
|
+
subject: rawKey,
|
|
630
|
+
layer: layer.id
|
|
631
|
+
});
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
636
|
+
if (canonical.declaredCategory === "secret") {
|
|
637
|
+
diagnostics.push({
|
|
638
|
+
code: "SECRET_ENTRY_KEY",
|
|
639
|
+
severity: "error",
|
|
640
|
+
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.`,
|
|
641
|
+
subject: rawKey,
|
|
642
|
+
layer: layer.id
|
|
643
|
+
});
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const unpacked = unpackEntryValue(value);
|
|
647
|
+
if (isVacuousWhen(unpacked.when)) {
|
|
648
|
+
diagnostics.push({
|
|
649
|
+
code: "EMPTY_WHEN",
|
|
650
|
+
severity: "warning",
|
|
651
|
+
message: `Entry key "${rawKey}" carries an empty "when" object, which is vacuously true and has no effect.`,
|
|
652
|
+
subject: rawKey,
|
|
653
|
+
layer: layer.id
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
const pattern = canonical.canonicalPattern;
|
|
657
|
+
const rule = {
|
|
658
|
+
rawKey,
|
|
659
|
+
declaredCategory: canonical.declaredCategory,
|
|
660
|
+
canonicalPattern: pattern,
|
|
661
|
+
value: unpacked.value,
|
|
662
|
+
...unpacked.when === void 0 ? {} : { when: unpacked.when },
|
|
663
|
+
layer: layer.id,
|
|
664
|
+
ordinal,
|
|
665
|
+
isExact: isExactPattern2(pattern),
|
|
666
|
+
literalPrefix: literalPrefixOf(pattern),
|
|
667
|
+
segmentCount: pattern.split("/").filter((segment) => segment !== "").length,
|
|
668
|
+
matches: compileMatcher(pattern)
|
|
669
|
+
};
|
|
670
|
+
rules.set(pattern, rule);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return { categories, rules, launch, diagnostics };
|
|
674
|
+
}
|
|
675
|
+
function orderedKeys(layer) {
|
|
676
|
+
const present = new Set(Object.keys(layer.entries ?? {}));
|
|
677
|
+
const ordered = [];
|
|
678
|
+
for (const key of layer.entryOrder ?? []) {
|
|
679
|
+
if (present.has(key)) {
|
|
680
|
+
ordered.push(key);
|
|
681
|
+
present.delete(key);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const key of present) {
|
|
685
|
+
ordered.push(key);
|
|
686
|
+
}
|
|
687
|
+
return ordered;
|
|
688
|
+
}
|
|
689
|
+
function matchingRules(flattened, relPath) {
|
|
690
|
+
const matches = [];
|
|
691
|
+
for (const rule of flattened.rules.values()) {
|
|
692
|
+
if (rule.matches(relPath)) {
|
|
693
|
+
matches.push(rule);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return matches.sort((a, b) => compareSpecificity(b, a));
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/resolve/decide.ts
|
|
700
|
+
function selectRule(relPath, flattened, context) {
|
|
701
|
+
const eliminated = [];
|
|
702
|
+
for (const rule of matchingRules(flattened, relPath)) {
|
|
703
|
+
const evaluation = evaluateWhen(rule.when, {
|
|
704
|
+
nowMs: context.nowMs,
|
|
705
|
+
...context.fact === void 0 ? {} : { fact: context.fact },
|
|
706
|
+
...context.branch === void 0 ? {} : { branch: context.branch },
|
|
707
|
+
...context.branchDetached === void 0 ? {} : { branchDetached: context.branchDetached },
|
|
708
|
+
env: context.env
|
|
709
|
+
});
|
|
710
|
+
if (evaluation.passed) {
|
|
711
|
+
return { rule, eliminated };
|
|
712
|
+
}
|
|
713
|
+
eliminated.push({ rule, failed: evaluation.failed });
|
|
714
|
+
}
|
|
715
|
+
return { eliminated };
|
|
716
|
+
}
|
|
717
|
+
function resolveEntry(relPath, params) {
|
|
718
|
+
const diagnostics = [];
|
|
719
|
+
const head = relPath.split("/")[0] ?? relPath;
|
|
720
|
+
const category = params.classification.get(head) ?? null;
|
|
721
|
+
if (category === "secret") {
|
|
722
|
+
for (const rule of matchingRules(params.flattened, relPath)) {
|
|
723
|
+
diagnostics.push({
|
|
724
|
+
code: "SECRET_PATH_NEUTRALISED",
|
|
725
|
+
severity: "warning",
|
|
726
|
+
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.`,
|
|
727
|
+
subject: relPath,
|
|
728
|
+
layer: rule.layer
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return { decision: { relPath, shared: false, via: "secret-floor", category }, diagnostics };
|
|
732
|
+
}
|
|
733
|
+
if (category === null) {
|
|
734
|
+
diagnostics.push({
|
|
735
|
+
code: "UNCLASSIFIED_ENTRY",
|
|
736
|
+
severity: "warning",
|
|
737
|
+
message: `"${head}" is not in the category map, so it is excluded. Run \`agent-shim configure\` to classify it, which records the answer in the local overlay without touching the shipped default map.`,
|
|
738
|
+
subject: head
|
|
739
|
+
});
|
|
740
|
+
return { decision: { relPath, shared: false, via: "unclassified", category }, diagnostics };
|
|
741
|
+
}
|
|
742
|
+
const fact = params.facts.entries.get(relPath);
|
|
743
|
+
const selection = selectRule(relPath, params.flattened, {
|
|
744
|
+
nowMs: params.facts.nowMs,
|
|
745
|
+
...fact === void 0 ? {} : { fact },
|
|
746
|
+
...params.facts.branch === void 0 ? {} : { branch: params.facts.branch },
|
|
747
|
+
...params.facts.branchDetached === void 0 ? {} : { branchDetached: params.facts.branchDetached },
|
|
748
|
+
env: params.facts.env
|
|
749
|
+
});
|
|
750
|
+
if (selection.rule !== void 0) {
|
|
751
|
+
const winner = selection.rule;
|
|
752
|
+
if (winner.declaredCategory !== category) {
|
|
753
|
+
diagnostics.push({
|
|
754
|
+
code: "CATEGORY_PREFIX_MISMATCH",
|
|
755
|
+
severity: "warning",
|
|
756
|
+
message: `Entry key "${winner.rawKey}" declares the ${winner.declaredCategory} category but matches "${relPath}", which is classified as ${category}.`,
|
|
757
|
+
subject: relPath,
|
|
758
|
+
layer: winner.layer
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
diagnostics.push(...reportOverriddenExactKeys(winner, params, relPath));
|
|
762
|
+
return {
|
|
763
|
+
decision: {
|
|
764
|
+
relPath,
|
|
765
|
+
shared: winner.value,
|
|
766
|
+
via: "entry-rule",
|
|
767
|
+
category,
|
|
768
|
+
rule: winner,
|
|
769
|
+
...selection.eliminated.length === 0 ? {} : { eliminated: selection.eliminated }
|
|
770
|
+
},
|
|
771
|
+
diagnostics
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
if (isOverridableCategory(category)) {
|
|
775
|
+
const override = params.flattened.categories.get(category);
|
|
776
|
+
if (override !== void 0) {
|
|
777
|
+
return {
|
|
778
|
+
decision: {
|
|
779
|
+
relPath,
|
|
780
|
+
shared: override,
|
|
781
|
+
via: "category-override",
|
|
782
|
+
category,
|
|
783
|
+
...selection.eliminated.length === 0 ? {} : { eliminated: selection.eliminated }
|
|
784
|
+
},
|
|
785
|
+
diagnostics
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
decision: {
|
|
791
|
+
relPath,
|
|
792
|
+
shared: SHIPPED_CATEGORY_DEFAULTS[category],
|
|
793
|
+
via: "category-default",
|
|
794
|
+
category,
|
|
795
|
+
...selection.eliminated.length === 0 ? {} : { eliminated: selection.eliminated }
|
|
796
|
+
},
|
|
797
|
+
diagnostics
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function reportOverriddenExactKeys(winner, params, relPath) {
|
|
801
|
+
if (winner.isExact) {
|
|
802
|
+
return [];
|
|
803
|
+
}
|
|
804
|
+
const diagnostics = [];
|
|
805
|
+
for (const candidate of params.flattened.rules.values()) {
|
|
806
|
+
if (candidate === winner || !candidate.isExact || candidate.layer >= winner.layer) {
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (!candidate.matches(relPath) || candidate.value === winner.value) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
diagnostics.push({
|
|
813
|
+
code: "EXACT_ENTRY_OVERRIDDEN_BY_LATER_GLOB",
|
|
814
|
+
severity: "info",
|
|
815
|
+
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.`,
|
|
816
|
+
subject: relPath,
|
|
817
|
+
layer: winner.layer
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
return diagnostics;
|
|
821
|
+
}
|
|
822
|
+
function resolveAll(params) {
|
|
823
|
+
const decisions = /* @__PURE__ */ new Map();
|
|
824
|
+
const diagnostics = [...params.flattened.diagnostics];
|
|
825
|
+
for (const relPath of params.facts.entries.keys()) {
|
|
826
|
+
const result = resolveEntry(relPath, params);
|
|
827
|
+
decisions.set(relPath, result.decision);
|
|
828
|
+
diagnostics.push(...result.diagnostics);
|
|
829
|
+
}
|
|
830
|
+
return { decisions, diagnostics: dedupeDiagnostics(diagnostics) };
|
|
831
|
+
}
|
|
832
|
+
function dedupeDiagnostics(diagnostics) {
|
|
833
|
+
const seen = /* @__PURE__ */ new Set();
|
|
834
|
+
const unique = [];
|
|
835
|
+
for (const diagnostic of diagnostics) {
|
|
836
|
+
const key = `${diagnostic.code} ${diagnostic.subject ?? ""} ${diagnostic.layer === void 0 ? "" : String(diagnostic.layer)} ${diagnostic.message}`;
|
|
837
|
+
if (seen.has(key)) {
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
seen.add(key);
|
|
841
|
+
unique.push(diagnostic);
|
|
842
|
+
}
|
|
843
|
+
return unique;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/resolve/plan.ts
|
|
847
|
+
import path2 from "node:path";
|
|
848
|
+
function parentOf(rel) {
|
|
849
|
+
const index = rel.lastIndexOf("/");
|
|
850
|
+
return index === -1 ? "" : rel.slice(0, index);
|
|
851
|
+
}
|
|
852
|
+
function buildChildIndex(facts) {
|
|
853
|
+
const children = /* @__PURE__ */ new Map();
|
|
854
|
+
for (const rel of facts.entries.keys()) {
|
|
855
|
+
const parent = parentOf(rel);
|
|
856
|
+
const bucket = children.get(parent);
|
|
857
|
+
if (bucket === void 0) {
|
|
858
|
+
children.set(parent, [rel]);
|
|
859
|
+
} else {
|
|
860
|
+
bucket.push(rel);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
for (const bucket of children.values()) {
|
|
864
|
+
bucket.sort();
|
|
865
|
+
}
|
|
866
|
+
return children;
|
|
867
|
+
}
|
|
868
|
+
function planFarm(params) {
|
|
869
|
+
const children = buildChildIndex(params.facts);
|
|
870
|
+
const entries = [];
|
|
871
|
+
const materialised = [];
|
|
872
|
+
const links = [];
|
|
873
|
+
const conditionalPatterns = [...params.flattened.rules.values()].filter((rule) => rule.when !== void 0).map((rule) => rule.canonicalPattern);
|
|
874
|
+
const conditionalRuleCouldReach = (dir) => conditionalPatterns.some((pattern) => patternCouldReachUnder(pattern, dir));
|
|
875
|
+
const subtreeDecision = (rel) => {
|
|
876
|
+
const own = params.decisions.get(rel);
|
|
877
|
+
let value = own?.shared;
|
|
878
|
+
const stack = [...children.get(rel) ?? []];
|
|
879
|
+
while (stack.length > 0) {
|
|
880
|
+
const next = stack.pop();
|
|
881
|
+
if (next === void 0) {
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const decision = params.decisions.get(next);
|
|
885
|
+
if (decision === void 0) {
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
if (value === void 0) {
|
|
889
|
+
value = decision.shared;
|
|
890
|
+
} else if (decision.shared !== value) {
|
|
891
|
+
return void 0;
|
|
892
|
+
}
|
|
893
|
+
stack.push(...children.get(next) ?? []);
|
|
894
|
+
}
|
|
895
|
+
return value;
|
|
896
|
+
};
|
|
897
|
+
const linkTarget = (rel) => path2.join(params.facts.claudeHome, rel);
|
|
898
|
+
const visit = (rel) => {
|
|
899
|
+
const fact = params.facts.entries.get(rel);
|
|
900
|
+
const decision = params.decisions.get(rel);
|
|
901
|
+
if (fact === void 0) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (!fact.isDirectory) {
|
|
905
|
+
if (decision?.shared === true) {
|
|
906
|
+
entries.push({ kind: "link", rel, target: linkTarget(rel) });
|
|
907
|
+
links.push({ rel, target: linkTarget(rel) });
|
|
908
|
+
} else {
|
|
909
|
+
entries.push({ kind: "omit", rel });
|
|
910
|
+
}
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (conditionalRuleCouldReach(rel)) {
|
|
914
|
+
entries.push({ kind: "materialise", rel, reason: "conditional-rule" });
|
|
915
|
+
materialised.push(rel);
|
|
916
|
+
for (const child of children.get(rel) ?? []) {
|
|
917
|
+
visit(child);
|
|
918
|
+
}
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
const uniform = subtreeDecision(rel);
|
|
922
|
+
if (uniform === true) {
|
|
923
|
+
entries.push({ kind: "link", rel, target: linkTarget(rel) });
|
|
924
|
+
links.push({ rel, target: linkTarget(rel) });
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (uniform === false) {
|
|
928
|
+
entries.push({ kind: "omit", rel });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
entries.push({ kind: "materialise", rel, reason: "split-decision" });
|
|
932
|
+
materialised.push(rel);
|
|
933
|
+
for (const child of children.get(rel) ?? []) {
|
|
934
|
+
visit(child);
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
for (const rel of children.get("") ?? []) {
|
|
938
|
+
visit(rel);
|
|
939
|
+
}
|
|
940
|
+
return { entries, materialised, links };
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/resolve/extends.ts
|
|
944
|
+
function lineariseProfile(rootName, load) {
|
|
945
|
+
const stack = [];
|
|
946
|
+
const visited = /* @__PURE__ */ new Set();
|
|
947
|
+
const order = [];
|
|
948
|
+
const diagnostics = [];
|
|
949
|
+
const visit = (name) => {
|
|
950
|
+
if (stack.includes(name)) {
|
|
951
|
+
diagnostics.push({
|
|
952
|
+
code: "EXTENDS_CYCLE",
|
|
953
|
+
severity: "error",
|
|
954
|
+
message: `Circular profile extends: ${[...stack, name].join(" -> ")}. The cycle is broken here and "${name}" is not applied twice.`,
|
|
955
|
+
subject: name
|
|
956
|
+
});
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (visited.has(name)) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const source = load(name);
|
|
963
|
+
if (source === void 0) {
|
|
964
|
+
diagnostics.push({
|
|
965
|
+
code: "MISSING_PROFILE",
|
|
966
|
+
severity: "error",
|
|
967
|
+
message: `Configuration profile "${name}" does not exist.`,
|
|
968
|
+
subject: name
|
|
969
|
+
});
|
|
970
|
+
visited.add(name);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
stack.push(name);
|
|
974
|
+
for (const dependency of source.profile.extends ?? []) {
|
|
975
|
+
visit(dependency);
|
|
976
|
+
}
|
|
977
|
+
stack.pop();
|
|
978
|
+
visited.add(name);
|
|
979
|
+
order.push(name);
|
|
980
|
+
};
|
|
981
|
+
visit(rootName);
|
|
982
|
+
return { order, diagnostics };
|
|
983
|
+
}
|
|
984
|
+
function profileLayers(rootName, load, startId) {
|
|
985
|
+
const { order, diagnostics } = lineariseProfile(rootName, load);
|
|
986
|
+
const layers = [];
|
|
987
|
+
let nextId = startId;
|
|
988
|
+
for (const name of order) {
|
|
989
|
+
const source = load(name);
|
|
990
|
+
if (source === void 0) {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
layers.push({
|
|
994
|
+
id: nextId,
|
|
995
|
+
kind: "config-profile",
|
|
996
|
+
source: source.filepath ?? name,
|
|
997
|
+
...source.profile.categories === void 0 ? {} : { categories: source.profile.categories },
|
|
998
|
+
...source.profile.entries === void 0 ? {} : { entries: source.profile.entries },
|
|
999
|
+
...source.entryOrder === void 0 ? {} : { entryOrder: source.entryOrder },
|
|
1000
|
+
...source.profile.launch === void 0 ? {} : { launch: source.profile.launch }
|
|
1001
|
+
});
|
|
1002
|
+
nextId += 1;
|
|
1003
|
+
}
|
|
1004
|
+
return { layers, nextId, diagnostics };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/resolve/walk.ts
|
|
1008
|
+
function assembleCascade(input) {
|
|
1009
|
+
const layers = [];
|
|
1010
|
+
const diagnostics = [];
|
|
1011
|
+
let nextId = 0;
|
|
1012
|
+
let identityPin;
|
|
1013
|
+
let directoryConfigProfile;
|
|
1014
|
+
if (input.globalConfig !== void 0) {
|
|
1015
|
+
const { config, entryOrder, filepath } = input.globalConfig;
|
|
1016
|
+
layers.push({
|
|
1017
|
+
id: nextId,
|
|
1018
|
+
kind: "global-config",
|
|
1019
|
+
source: filepath,
|
|
1020
|
+
...config.categories === void 0 ? {} : { categories: config.categories },
|
|
1021
|
+
...config.entries === void 0 ? {} : { entries: config.entries },
|
|
1022
|
+
...entryOrder === void 0 ? {} : { entryOrder },
|
|
1023
|
+
...config.launch === void 0 ? {} : { launch: config.launch }
|
|
1024
|
+
});
|
|
1025
|
+
nextId += 1;
|
|
1026
|
+
}
|
|
1027
|
+
if (input.baseConfigProfile !== void 0) {
|
|
1028
|
+
const resolved = profileLayers(input.baseConfigProfile, input.loadProfile, nextId);
|
|
1029
|
+
layers.push(...resolved.layers);
|
|
1030
|
+
diagnostics.push(...resolved.diagnostics);
|
|
1031
|
+
nextId = resolved.nextId;
|
|
1032
|
+
}
|
|
1033
|
+
for (const level of input.levels ?? []) {
|
|
1034
|
+
const sources = [];
|
|
1035
|
+
if (level.portable !== void 0) {
|
|
1036
|
+
sources.push({ kind: "portable", config: level.portable.config, ...level.portable.entryOrder === void 0 ? {} : { entryOrder: level.portable.entryOrder }, filepath: level.portable.filepath });
|
|
1037
|
+
}
|
|
1038
|
+
for (const entry of level.rules ?? []) {
|
|
1039
|
+
sources.push({ kind: "directory-rule", config: entry.rule, ...entry.entryOrder === void 0 ? {} : { entryOrder: entry.entryOrder }, filepath: entry.filepath });
|
|
1040
|
+
}
|
|
1041
|
+
if (level.portableLocal !== void 0) {
|
|
1042
|
+
sources.push({ kind: "portable-local", config: level.portableLocal.config, ...level.portableLocal.entryOrder === void 0 ? {} : { entryOrder: level.portableLocal.entryOrder }, filepath: level.portableLocal.filepath });
|
|
1043
|
+
}
|
|
1044
|
+
for (const source of sources) {
|
|
1045
|
+
if (source.config.identity !== void 0) {
|
|
1046
|
+
identityPin = source.config.identity;
|
|
1047
|
+
}
|
|
1048
|
+
if (source.config.configProfile !== void 0) {
|
|
1049
|
+
directoryConfigProfile = source.config.configProfile;
|
|
1050
|
+
const resolved = profileLayers(source.config.configProfile, input.loadProfile, nextId);
|
|
1051
|
+
layers.push(...resolved.layers);
|
|
1052
|
+
diagnostics.push(...resolved.diagnostics);
|
|
1053
|
+
nextId = resolved.nextId;
|
|
1054
|
+
}
|
|
1055
|
+
layers.push({
|
|
1056
|
+
id: nextId,
|
|
1057
|
+
kind: source.kind,
|
|
1058
|
+
source: source.filepath,
|
|
1059
|
+
...source.config.categories === void 0 ? {} : { categories: source.config.categories },
|
|
1060
|
+
...source.config.entries === void 0 ? {} : { entries: source.config.entries },
|
|
1061
|
+
...source.entryOrder === void 0 ? {} : { entryOrder: source.entryOrder },
|
|
1062
|
+
...source.config.launch === void 0 ? {} : { launch: source.config.launch }
|
|
1063
|
+
});
|
|
1064
|
+
nextId += 1;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (input.cliOverride !== void 0) {
|
|
1068
|
+
layers.push({
|
|
1069
|
+
id: nextId,
|
|
1070
|
+
kind: "cli-override",
|
|
1071
|
+
source: "command line",
|
|
1072
|
+
...input.cliOverride.categories === void 0 ? {} : { categories: input.cliOverride.categories },
|
|
1073
|
+
...input.cliOverride.entries === void 0 ? {} : { entries: input.cliOverride.entries },
|
|
1074
|
+
...input.cliOverride.entryOrder === void 0 ? {} : { entryOrder: input.cliOverride.entryOrder },
|
|
1075
|
+
...input.cliOverride.launch === void 0 ? {} : { launch: input.cliOverride.launch }
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
return {
|
|
1079
|
+
layers,
|
|
1080
|
+
diagnostics,
|
|
1081
|
+
...identityPin === void 0 ? {} : { identityPin },
|
|
1082
|
+
...directoryConfigProfile === void 0 ? {} : { directoryConfigProfile }
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/resolve/pipeline.ts
|
|
1087
|
+
function topLevelNames(facts) {
|
|
1088
|
+
const names = /* @__PURE__ */ new Set();
|
|
1089
|
+
for (const rel of facts.entries.keys()) {
|
|
1090
|
+
const head = rel.split("/")[0];
|
|
1091
|
+
if (head !== void 0 && head !== "") {
|
|
1092
|
+
names.add(head);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return [...names].sort();
|
|
1096
|
+
}
|
|
1097
|
+
function resolveDecisions(input) {
|
|
1098
|
+
const assembled = assembleCascade(input.cascade);
|
|
1099
|
+
const flattened = flattenLayers(assembled.layers, { home: input.facts.home });
|
|
1100
|
+
const classification = classifyEntries(topLevelNames(input.facts), {
|
|
1101
|
+
defaults: input.classification.defaults,
|
|
1102
|
+
...input.classification.overlay === void 0 ? {} : { overlay: input.classification.overlay }
|
|
1103
|
+
});
|
|
1104
|
+
const resolved = resolveAll({
|
|
1105
|
+
flattened,
|
|
1106
|
+
facts: input.facts,
|
|
1107
|
+
classification: classification.classification
|
|
1108
|
+
});
|
|
1109
|
+
const farm = planFarm({ facts: input.facts, decisions: resolved.decisions, flattened });
|
|
1110
|
+
return {
|
|
1111
|
+
assembled,
|
|
1112
|
+
flattened,
|
|
1113
|
+
classification,
|
|
1114
|
+
decisions: resolved.decisions,
|
|
1115
|
+
farm,
|
|
1116
|
+
diagnostics: [...assembled.diagnostics, ...resolved.diagnostics]
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/launcher/farm.ts
|
|
1121
|
+
import path4 from "node:path";
|
|
1122
|
+
import { z as z2 } from "zod";
|
|
1123
|
+
|
|
1124
|
+
// src/resolve/reconcile.ts
|
|
1125
|
+
function parentOf2(rel) {
|
|
1126
|
+
const index = rel.lastIndexOf("/");
|
|
1127
|
+
return index === -1 ? "" : rel.slice(0, index);
|
|
1128
|
+
}
|
|
1129
|
+
function indexByParent(listing) {
|
|
1130
|
+
const children = /* @__PURE__ */ new Map();
|
|
1131
|
+
for (const entry of listing) {
|
|
1132
|
+
const parent = parentOf2(entry.rel);
|
|
1133
|
+
const bucket = children.get(parent);
|
|
1134
|
+
if (bucket === void 0) {
|
|
1135
|
+
children.set(parent, [entry]);
|
|
1136
|
+
} else {
|
|
1137
|
+
bucket.push(entry);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
for (const bucket of children.values()) {
|
|
1141
|
+
bucket.sort((a, b) => a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0);
|
|
1142
|
+
}
|
|
1143
|
+
return children;
|
|
1144
|
+
}
|
|
1145
|
+
function planReconciliation(params) {
|
|
1146
|
+
const conservative = params.manifest === void 0;
|
|
1147
|
+
const diagnostics = [];
|
|
1148
|
+
const actions = [];
|
|
1149
|
+
const farmChildren = indexByParent(params.farmListing);
|
|
1150
|
+
const canonicalByRel = new Map(params.canonicalListing.map((entry) => [entry.rel, entry]));
|
|
1151
|
+
const materialised = /* @__PURE__ */ new Set([""]);
|
|
1152
|
+
if (params.manifest !== void 0) {
|
|
1153
|
+
for (const rel of params.manifest.materialised) {
|
|
1154
|
+
materialised.add(rel);
|
|
1155
|
+
}
|
|
1156
|
+
} else {
|
|
1157
|
+
for (const entry of params.farmListing) {
|
|
1158
|
+
if (entry.kind === "dir") {
|
|
1159
|
+
materialised.add(entry.rel);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
diagnostics.push({
|
|
1163
|
+
code: "FARM_MANIFEST_MISSING",
|
|
1164
|
+
severity: "warning",
|
|
1165
|
+
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."
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
const visit = (dir) => {
|
|
1169
|
+
for (const child of farmChildren.get(dir) ?? []) {
|
|
1170
|
+
if (child.kind === "symlink") {
|
|
1171
|
+
actions.push({ kind: "skip", rel: child.rel, reason: "is-symlink" });
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
if (child.kind === "dir" && materialised.has(child.rel)) {
|
|
1175
|
+
actions.push({ kind: "skip", rel: child.rel, reason: "tracked-materialised-dir" });
|
|
1176
|
+
visit(child.rel);
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
if (child.kind === "dir") {
|
|
1180
|
+
const grandchildren = farmChildren.get(child.rel) ?? [];
|
|
1181
|
+
if (grandchildren.length === 0) {
|
|
1182
|
+
if (canonicalByRel.has(child.rel)) {
|
|
1183
|
+
actions.push({ kind: "skip", rel: child.rel, reason: "identical-content" });
|
|
1184
|
+
} else {
|
|
1185
|
+
actions.push({ kind: "adopt", rel: child.rel, reason: "absent-from-canonical" });
|
|
1186
|
+
}
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
visit(child.rel);
|
|
1190
|
+
continue;
|
|
1191
|
+
}
|
|
1192
|
+
const canonical = canonicalByRel.get(child.rel);
|
|
1193
|
+
if (canonical === void 0) {
|
|
1194
|
+
actions.push({ kind: "adopt", rel: child.rel, reason: "absent-from-canonical" });
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (canonical.contentHash !== void 0 && canonical.contentHash === child.contentHash) {
|
|
1198
|
+
actions.push({ kind: "skip", rel: child.rel, reason: "identical-content" });
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
actions.push({ kind: "adopt-conflict", rel: child.rel });
|
|
1202
|
+
diagnostics.push({
|
|
1203
|
+
code: "RECONCILE_CONFLICT",
|
|
1204
|
+
severity: "warning",
|
|
1205
|
+
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.`,
|
|
1206
|
+
subject: child.rel
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
visit("");
|
|
1211
|
+
return { actions, diagnostics, conservative };
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// src/legacy.ts
|
|
1215
|
+
var LEGACY_FARM_MANIFEST_FILENAME = ".claude-use-farm.json";
|
|
1216
|
+
|
|
1217
|
+
// src/launcher/lock.ts
|
|
1218
|
+
import { randomUUID } from "node:crypto";
|
|
1219
|
+
import path3 from "node:path";
|
|
1220
|
+
var DEFAULT_STALE_AFTER_MS = 12e4;
|
|
1221
|
+
var DEFAULT_RETRY_DELAY_MS = 50;
|
|
1222
|
+
var DEFAULT_MAX_ATTEMPTS = 200;
|
|
1223
|
+
var IdentityLockBusyError = class extends CliError {
|
|
1224
|
+
constructor(identity, lockPath, holderPid) {
|
|
1225
|
+
super(
|
|
1226
|
+
`Another agent-shim 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.`
|
|
1227
|
+
);
|
|
1228
|
+
this.identity = identity;
|
|
1229
|
+
this.lockPath = lockPath;
|
|
1230
|
+
this.holderPid = holderPid;
|
|
1231
|
+
this.name = "IdentityLockBusyError";
|
|
1232
|
+
}
|
|
1233
|
+
identity;
|
|
1234
|
+
lockPath;
|
|
1235
|
+
holderPid;
|
|
1236
|
+
};
|
|
1237
|
+
function identityLockPath(dir, identity) {
|
|
1238
|
+
return path3.join(dir, `.${identity}.lock`);
|
|
1239
|
+
}
|
|
1240
|
+
function parseLockRecord(raw) {
|
|
1241
|
+
if (raw === void 0) {
|
|
1242
|
+
return void 0;
|
|
1243
|
+
}
|
|
1244
|
+
let parsed;
|
|
1245
|
+
try {
|
|
1246
|
+
parsed = JSON.parse(raw);
|
|
1247
|
+
} catch {
|
|
1248
|
+
return void 0;
|
|
1249
|
+
}
|
|
1250
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1251
|
+
return void 0;
|
|
1252
|
+
}
|
|
1253
|
+
if (!("identity" in parsed) || !("pid" in parsed) || !("token" in parsed) || !("acquiredAtMs" in parsed)) {
|
|
1254
|
+
return void 0;
|
|
1255
|
+
}
|
|
1256
|
+
const { identity, pid, token, acquiredAtMs } = parsed;
|
|
1257
|
+
if (typeof identity !== "string" || typeof pid !== "number" || typeof token !== "string" || typeof acquiredAtMs !== "number") {
|
|
1258
|
+
return void 0;
|
|
1259
|
+
}
|
|
1260
|
+
return { identity, pid, token, acquiredAtMs };
|
|
1261
|
+
}
|
|
1262
|
+
function acquireIdentityLock(params) {
|
|
1263
|
+
const lockPath = identityLockPath(params.dir, params.identity);
|
|
1264
|
+
const staleAfterMs = params.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
1265
|
+
const retryDelayMs = params.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
|
|
1266
|
+
const maxAttempts = params.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
1267
|
+
const token = randomUUID();
|
|
1268
|
+
if (params.fs.lstat(params.dir) === void 0) {
|
|
1269
|
+
params.fs.mkdirp(params.dir);
|
|
1270
|
+
}
|
|
1271
|
+
let lastHolderPid;
|
|
1272
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
1273
|
+
const record = { identity: params.identity, pid: params.pid, token, acquiredAtMs: params.nowMs() };
|
|
1274
|
+
if (params.fs.writeFileExclusive(lockPath, `${JSON.stringify(record)}
|
|
1275
|
+
`)) {
|
|
1276
|
+
return {
|
|
1277
|
+
path: lockPath,
|
|
1278
|
+
token,
|
|
1279
|
+
release: () => {
|
|
1280
|
+
const current = parseLockRecord(params.fs.readFileUtf8(lockPath));
|
|
1281
|
+
if (current !== void 0 && current.token !== token) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
params.fs.removeRecursive(lockPath);
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
const existing = parseLockRecord(params.fs.readFileUtf8(lockPath));
|
|
1289
|
+
if (existing === void 0) {
|
|
1290
|
+
params.fs.removeRecursive(lockPath);
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
lastHolderPid = existing.pid;
|
|
1294
|
+
const expired = params.nowMs() - existing.acquiredAtMs > staleAfterMs;
|
|
1295
|
+
if (expired || !params.isRunning(existing.pid)) {
|
|
1296
|
+
params.fs.removeRecursive(lockPath);
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
params.sleep(retryDelayMs);
|
|
1300
|
+
}
|
|
1301
|
+
throw new IdentityLockBusyError(params.identity, lockPath, lastHolderPid);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/launcher/farm.ts
|
|
1305
|
+
var FARM_MANIFEST_FILENAME = ".agent-shim-farm.json";
|
|
1306
|
+
var FarmManifestSchema = z2.strictObject({
|
|
1307
|
+
version: z2.literal(1),
|
|
1308
|
+
builtAtMs: z2.number(),
|
|
1309
|
+
identity: z2.string(),
|
|
1310
|
+
configProfile: z2.string().optional(),
|
|
1311
|
+
cwd: z2.string(),
|
|
1312
|
+
claudeHome: z2.string(),
|
|
1313
|
+
materialised: z2.array(z2.string()),
|
|
1314
|
+
links: z2.array(z2.strictObject({ rel: z2.string(), target: z2.string() }))
|
|
1315
|
+
});
|
|
1316
|
+
function readFarmManifest(fs2, farmRoot) {
|
|
1317
|
+
const raw = fs2.readFileUtf8(path4.join(farmRoot, FARM_MANIFEST_FILENAME)) ?? fs2.readFileUtf8(path4.join(farmRoot, LEGACY_FARM_MANIFEST_FILENAME));
|
|
1318
|
+
if (raw === void 0) {
|
|
1319
|
+
return void 0;
|
|
1320
|
+
}
|
|
1321
|
+
let parsed;
|
|
1322
|
+
try {
|
|
1323
|
+
parsed = JSON.parse(raw);
|
|
1324
|
+
} catch {
|
|
1325
|
+
return void 0;
|
|
1326
|
+
}
|
|
1327
|
+
const result = FarmManifestSchema.safeParse(parsed);
|
|
1328
|
+
return result.success ? result.data : void 0;
|
|
1329
|
+
}
|
|
1330
|
+
function buildEntryFacts(params) {
|
|
1331
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1332
|
+
const visit = (rel) => {
|
|
1333
|
+
const absolute = path4.join(params.claudeHome, rel);
|
|
1334
|
+
const stat = params.fs.lstat(absolute);
|
|
1335
|
+
if (stat === void 0) {
|
|
1336
|
+
return void 0;
|
|
1337
|
+
}
|
|
1338
|
+
const isDirectory = stat.kind === "dir";
|
|
1339
|
+
let latestMtimeMs = stat.mtimeMs;
|
|
1340
|
+
let totalSizeBytes = stat.sizeBytes;
|
|
1341
|
+
if (isDirectory) {
|
|
1342
|
+
for (const name of [...params.fs.readdir(absolute)].sort()) {
|
|
1343
|
+
const child = visit(`${rel}/${name}`);
|
|
1344
|
+
if (child !== void 0) {
|
|
1345
|
+
latestMtimeMs = Math.max(latestMtimeMs, child.latestMtimeMs);
|
|
1346
|
+
totalSizeBytes += child.totalSizeBytes;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
entries.set(rel, {
|
|
1351
|
+
relPath: rel,
|
|
1352
|
+
isDirectory,
|
|
1353
|
+
isSymlink: stat.kind === "symlink",
|
|
1354
|
+
mtimeMs: stat.mtimeMs,
|
|
1355
|
+
latestMtimeMs,
|
|
1356
|
+
sizeBytes: stat.sizeBytes,
|
|
1357
|
+
totalSizeBytes
|
|
1358
|
+
});
|
|
1359
|
+
return { latestMtimeMs, totalSizeBytes };
|
|
1360
|
+
};
|
|
1361
|
+
for (const name of [...params.fs.readdir(params.claudeHome)].sort()) {
|
|
1362
|
+
visit(name);
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
nowMs: params.nowMs,
|
|
1366
|
+
home: params.home,
|
|
1367
|
+
claudeHome: params.claudeHome,
|
|
1368
|
+
cwd: params.cwd,
|
|
1369
|
+
...params.branch === void 0 ? {} : { branch: params.branch },
|
|
1370
|
+
...params.branchDetached === void 0 ? {} : { branchDetached: params.branchDetached },
|
|
1371
|
+
env: params.env,
|
|
1372
|
+
entries
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
function listSubtree(fs2, root, rel) {
|
|
1376
|
+
const absolute = path4.join(root, rel);
|
|
1377
|
+
const stat = fs2.lstat(absolute);
|
|
1378
|
+
if (stat === void 0) {
|
|
1379
|
+
return [];
|
|
1380
|
+
}
|
|
1381
|
+
if (stat.kind === "symlink") {
|
|
1382
|
+
return [{ rel, kind: "symlink" }];
|
|
1383
|
+
}
|
|
1384
|
+
if (stat.kind === "dir") {
|
|
1385
|
+
const children = [...fs2.readdir(absolute)].sort().flatMap((name) => listSubtree(fs2, root, `${rel}/${name}`));
|
|
1386
|
+
return [{ rel, kind: "dir" }, ...children];
|
|
1387
|
+
}
|
|
1388
|
+
const contentHash = fs2.hashFile(absolute);
|
|
1389
|
+
return [{ rel, kind: "file", ...contentHash === void 0 ? {} : { contentHash } }];
|
|
1390
|
+
}
|
|
1391
|
+
function topLevelScopeRoots(roots) {
|
|
1392
|
+
const sorted = [...new Set(roots)].sort();
|
|
1393
|
+
const kept = [];
|
|
1394
|
+
for (const root of sorted) {
|
|
1395
|
+
if (kept.some((existing) => root === existing || root.startsWith(`${existing}/`))) {
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
kept.push(root);
|
|
1399
|
+
}
|
|
1400
|
+
return kept;
|
|
1401
|
+
}
|
|
1402
|
+
function reconciliationScope(fs2, farmRoot, manifest) {
|
|
1403
|
+
if (manifest !== void 0) {
|
|
1404
|
+
return topLevelScopeRoots(manifest.materialised);
|
|
1405
|
+
}
|
|
1406
|
+
return [...fs2.readdir(farmRoot)].filter((name) => name !== FARM_MANIFEST_FILENAME && name !== LEGACY_FARM_MANIFEST_FILENAME && fs2.lstat(path4.join(farmRoot, name))?.kind === "dir").sort();
|
|
1407
|
+
}
|
|
1408
|
+
function collectFarmListing(fs2, farmRoot, scopeRoots) {
|
|
1409
|
+
return scopeRoots.flatMap((root) => listSubtree(fs2, farmRoot, root));
|
|
1410
|
+
}
|
|
1411
|
+
function collectCanonicalCounterparts(fs2, claudeHome, farmListing) {
|
|
1412
|
+
const out = [];
|
|
1413
|
+
for (const entry of farmListing) {
|
|
1414
|
+
const absolute = path4.join(claudeHome, entry.rel);
|
|
1415
|
+
const stat = fs2.lstat(absolute);
|
|
1416
|
+
if (stat === void 0) {
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
if (stat.kind === "file") {
|
|
1420
|
+
const contentHash = fs2.hashFile(absolute);
|
|
1421
|
+
out.push({ rel: entry.rel, kind: "file", ...contentHash === void 0 ? {} : { contentHash } });
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
out.push({ rel: entry.rel, kind: stat.kind });
|
|
1425
|
+
}
|
|
1426
|
+
return out;
|
|
1427
|
+
}
|
|
1428
|
+
function carryOver(params) {
|
|
1429
|
+
const manifest = readFarmManifest(params.fs, params.previousRoot);
|
|
1430
|
+
const accounted = /* @__PURE__ */ new Set([FARM_MANIFEST_FILENAME, LEGACY_FARM_MANIFEST_FILENAME]);
|
|
1431
|
+
for (const rel of manifest?.materialised ?? []) {
|
|
1432
|
+
const head = rel.split("/")[0];
|
|
1433
|
+
if (head !== void 0 && head !== "") {
|
|
1434
|
+
accounted.add(head);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const carried = [];
|
|
1438
|
+
const autoResolved = [];
|
|
1439
|
+
const collided = [];
|
|
1440
|
+
for (const name of [...params.fs.readdir(params.previousRoot)].sort()) {
|
|
1441
|
+
if (accounted.has(name)) {
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
const stat = params.fs.lstat(path4.join(params.previousRoot, name));
|
|
1445
|
+
if (stat === void 0 || stat.kind === "symlink") {
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
if (params.fs.lstat(path4.join(params.farmRoot, name)) !== void 0) {
|
|
1449
|
+
const category = params.classification === void 0 ? void 0 : classifyEntries([name], params.classification).classification.get(name);
|
|
1450
|
+
if (category === "runtime") {
|
|
1451
|
+
params.fs.removeRecursive(path4.join(params.previousRoot, name));
|
|
1452
|
+
autoResolved.push(name);
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
collided.push(name);
|
|
1456
|
+
continue;
|
|
1457
|
+
}
|
|
1458
|
+
params.fs.rename(path4.join(params.previousRoot, name), path4.join(params.farmRoot, name));
|
|
1459
|
+
carried.push(name);
|
|
1460
|
+
}
|
|
1461
|
+
return { carried, autoResolved, collided };
|
|
1462
|
+
}
|
|
1463
|
+
function buildScratchTree(params) {
|
|
1464
|
+
params.fs.mkdirp(params.scratchRoot);
|
|
1465
|
+
for (const entry of params.plan.entries) {
|
|
1466
|
+
const absolute = path4.join(params.scratchRoot, entry.rel);
|
|
1467
|
+
if (entry.kind === "link") {
|
|
1468
|
+
params.fs.mkdirp(path4.dirname(absolute));
|
|
1469
|
+
params.fs.symlink(entry.target, absolute);
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
if (entry.kind === "materialise") {
|
|
1473
|
+
params.fs.mkdirp(absolute);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
params.fs.writeFileUtf8(
|
|
1477
|
+
path4.join(params.scratchRoot, FARM_MANIFEST_FILENAME),
|
|
1478
|
+
`${JSON.stringify(params.manifest, null, 2)}
|
|
1479
|
+
`
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
function swapIn(params) {
|
|
1483
|
+
if (params.fs.lstat(params.farmRoot) === void 0) {
|
|
1484
|
+
params.fs.mkdirp(path4.dirname(params.farmRoot));
|
|
1485
|
+
params.fs.rename(params.scratchRoot, params.farmRoot);
|
|
1486
|
+
return { carried: [], autoResolved: [], collided: [] };
|
|
1487
|
+
}
|
|
1488
|
+
params.fs.rename(params.farmRoot, params.previousRoot);
|
|
1489
|
+
params.fs.rename(params.scratchRoot, params.farmRoot);
|
|
1490
|
+
const result = carryOver({
|
|
1491
|
+
fs: params.fs,
|
|
1492
|
+
previousRoot: params.previousRoot,
|
|
1493
|
+
farmRoot: params.farmRoot,
|
|
1494
|
+
...params.classification === void 0 ? {} : { classification: params.classification }
|
|
1495
|
+
});
|
|
1496
|
+
if (result.collided.length === 0) {
|
|
1497
|
+
params.fs.removeRecursive(params.previousRoot);
|
|
1498
|
+
return result;
|
|
1499
|
+
}
|
|
1500
|
+
return { ...result, retainedPrevious: params.previousRoot };
|
|
1501
|
+
}
|
|
1502
|
+
function recoverInterruptedSwap(params) {
|
|
1503
|
+
const scratchPrefix = `.${params.identity}.scratch.`;
|
|
1504
|
+
const previousPrefix = `.${params.identity}.previous.`;
|
|
1505
|
+
const names = [...params.fs.readdir(params.identitiesDir)].sort();
|
|
1506
|
+
const removedScratch = [];
|
|
1507
|
+
for (const name of names.filter((candidate) => candidate.startsWith(scratchPrefix))) {
|
|
1508
|
+
params.fs.removeRecursive(path4.join(params.identitiesDir, name));
|
|
1509
|
+
removedScratch.push(name);
|
|
1510
|
+
}
|
|
1511
|
+
let previous = names.filter((candidate) => candidate.startsWith(previousPrefix));
|
|
1512
|
+
let restoredFrom;
|
|
1513
|
+
const first = previous[0];
|
|
1514
|
+
if (first !== void 0 && params.fs.lstat(params.farmRoot) === void 0) {
|
|
1515
|
+
params.fs.rename(path4.join(params.identitiesDir, first), params.farmRoot);
|
|
1516
|
+
restoredFrom = first;
|
|
1517
|
+
previous = previous.slice(1);
|
|
1518
|
+
}
|
|
1519
|
+
const completed = [];
|
|
1520
|
+
const autoResolved = [];
|
|
1521
|
+
const retained = [];
|
|
1522
|
+
for (const name of previous) {
|
|
1523
|
+
const previousRoot = path4.join(params.identitiesDir, name);
|
|
1524
|
+
const result = carryOver({
|
|
1525
|
+
fs: params.fs,
|
|
1526
|
+
previousRoot,
|
|
1527
|
+
farmRoot: params.farmRoot,
|
|
1528
|
+
...params.classification === void 0 ? {} : { classification: params.classification }
|
|
1529
|
+
});
|
|
1530
|
+
autoResolved.push(...result.autoResolved);
|
|
1531
|
+
if (result.collided.length === 0) {
|
|
1532
|
+
params.fs.removeRecursive(previousRoot);
|
|
1533
|
+
completed.push(name);
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
retained.push(previousRoot);
|
|
1537
|
+
}
|
|
1538
|
+
return {
|
|
1539
|
+
removedScratch,
|
|
1540
|
+
...restoredFrom === void 0 ? {} : { restoredFrom },
|
|
1541
|
+
completed,
|
|
1542
|
+
autoResolved,
|
|
1543
|
+
retained,
|
|
1544
|
+
recovered: removedScratch.length > 0 || restoredFrom !== void 0 || completed.length > 0 || autoResolved.length > 0 || retained.length > 0
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
function recoverFarm(params) {
|
|
1548
|
+
const farmRoot = path4.join(params.identitiesDir, params.identity);
|
|
1549
|
+
const lock = acquireIdentityLock({
|
|
1550
|
+
identity: params.identity,
|
|
1551
|
+
dir: params.identitiesDir,
|
|
1552
|
+
fs: params.fs,
|
|
1553
|
+
nowMs: params.now,
|
|
1554
|
+
pid: params.lock.pid,
|
|
1555
|
+
isRunning: params.lock.isRunning,
|
|
1556
|
+
sleep: params.lock.sleep,
|
|
1557
|
+
...params.lock.staleAfterMs === void 0 ? {} : { staleAfterMs: params.lock.staleAfterMs },
|
|
1558
|
+
...params.lock.retryDelayMs === void 0 ? {} : { retryDelayMs: params.lock.retryDelayMs },
|
|
1559
|
+
...params.lock.maxAttempts === void 0 ? {} : { maxAttempts: params.lock.maxAttempts }
|
|
1560
|
+
});
|
|
1561
|
+
try {
|
|
1562
|
+
return recoverInterruptedSwap({
|
|
1563
|
+
fs: params.fs,
|
|
1564
|
+
identitiesDir: params.identitiesDir,
|
|
1565
|
+
identity: params.identity,
|
|
1566
|
+
farmRoot,
|
|
1567
|
+
...params.classification === void 0 ? {} : { classification: params.classification }
|
|
1568
|
+
});
|
|
1569
|
+
} finally {
|
|
1570
|
+
lock.release();
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function executeReconciliation(params) {
|
|
1574
|
+
const heads = /* @__PURE__ */ new Set();
|
|
1575
|
+
for (const action of params.actions) {
|
|
1576
|
+
if (action.kind === "skip") {
|
|
1577
|
+
continue;
|
|
1578
|
+
}
|
|
1579
|
+
const head = action.rel.split("/")[0];
|
|
1580
|
+
if (head !== void 0 && head !== "") {
|
|
1581
|
+
heads.add(head);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
const classified = classifyEntries([...heads], {
|
|
1585
|
+
defaults: params.classification.defaults,
|
|
1586
|
+
...params.classification.overlay === void 0 ? {} : { overlay: params.classification.overlay }
|
|
1587
|
+
});
|
|
1588
|
+
const adopted = [];
|
|
1589
|
+
const conflicts = [];
|
|
1590
|
+
const blocked = [];
|
|
1591
|
+
const diagnostics = [];
|
|
1592
|
+
for (const action of params.actions) {
|
|
1593
|
+
if (action.kind === "skip") {
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
const head = action.rel.split("/")[0] ?? "";
|
|
1597
|
+
if (classified.classification.get(head) === "secret") {
|
|
1598
|
+
blocked.push(action.rel);
|
|
1599
|
+
diagnostics.push({
|
|
1600
|
+
code: "RECONCILE_SECRET_BLOCKED",
|
|
1601
|
+
severity: "warning",
|
|
1602
|
+
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.`,
|
|
1603
|
+
subject: action.rel
|
|
1604
|
+
});
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
const from = path4.join(params.farmRoot, action.rel);
|
|
1608
|
+
const canonical = path4.join(params.claudeHome, action.rel);
|
|
1609
|
+
if (action.kind === "adopt") {
|
|
1610
|
+
params.fs.mkdirp(path4.dirname(canonical));
|
|
1611
|
+
params.fs.copyRecursive(from, canonical);
|
|
1612
|
+
adopted.push(action.rel);
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
const preserved = `${canonical}.farm-conflict-${String(params.nowMs)}`;
|
|
1616
|
+
params.fs.mkdirp(path4.dirname(canonical));
|
|
1617
|
+
params.fs.copyRecursive(from, preserved);
|
|
1618
|
+
conflicts.push(action.rel);
|
|
1619
|
+
}
|
|
1620
|
+
return { adopted, conflicts, blocked, diagnostics };
|
|
1621
|
+
}
|
|
1622
|
+
function sameLinks(a, b) {
|
|
1623
|
+
if (a.length !== b.length) {
|
|
1624
|
+
return false;
|
|
1625
|
+
}
|
|
1626
|
+
return a.every((link, index) => {
|
|
1627
|
+
const other = b[index];
|
|
1628
|
+
if (other === void 0) {
|
|
1629
|
+
return false;
|
|
1630
|
+
}
|
|
1631
|
+
return other.rel === link.rel && other.target === link.target;
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
function sameStrings(a, b) {
|
|
1635
|
+
return a.length === b.length && a.every((value, index) => b[index] === value);
|
|
1636
|
+
}
|
|
1637
|
+
function farmAlreadyMatches(existing, next) {
|
|
1638
|
+
return existing.identity === next.identity && existing.claudeHome === next.claudeHome && sameStrings(existing.materialised, next.materialised) && sameLinks(existing.links, next.links);
|
|
1639
|
+
}
|
|
1640
|
+
function resyncFarm(params) {
|
|
1641
|
+
const farmRoot = path4.join(params.identitiesDir, params.identity);
|
|
1642
|
+
const lock = acquireIdentityLock({
|
|
1643
|
+
identity: params.identity,
|
|
1644
|
+
dir: params.identitiesDir,
|
|
1645
|
+
fs: params.fs,
|
|
1646
|
+
nowMs: params.now,
|
|
1647
|
+
pid: params.lock.pid,
|
|
1648
|
+
isRunning: params.lock.isRunning,
|
|
1649
|
+
sleep: params.lock.sleep,
|
|
1650
|
+
...params.lock.staleAfterMs === void 0 ? {} : { staleAfterMs: params.lock.staleAfterMs },
|
|
1651
|
+
...params.lock.retryDelayMs === void 0 ? {} : { retryDelayMs: params.lock.retryDelayMs },
|
|
1652
|
+
...params.lock.maxAttempts === void 0 ? {} : { maxAttempts: params.lock.maxAttempts }
|
|
1653
|
+
});
|
|
1654
|
+
try {
|
|
1655
|
+
const recovery = recoverInterruptedSwap({
|
|
1656
|
+
fs: params.fs,
|
|
1657
|
+
identitiesDir: params.identitiesDir,
|
|
1658
|
+
identity: params.identity,
|
|
1659
|
+
farmRoot,
|
|
1660
|
+
classification: params.classification
|
|
1661
|
+
});
|
|
1662
|
+
const previousManifest = readFarmManifest(params.fs, farmRoot);
|
|
1663
|
+
const scope = reconciliationScope(params.fs, farmRoot, previousManifest);
|
|
1664
|
+
const farmListing = collectFarmListing(params.fs, farmRoot, scope);
|
|
1665
|
+
const canonicalListing = collectCanonicalCounterparts(params.fs, params.claudeHome, farmListing);
|
|
1666
|
+
const reconciliation = scope.length === 0 ? { actions: [], diagnostics: [], conservative: false } : planReconciliation({
|
|
1667
|
+
...previousManifest === void 0 ? {} : { manifest: previousManifest },
|
|
1668
|
+
farmListing,
|
|
1669
|
+
canonicalListing
|
|
1670
|
+
});
|
|
1671
|
+
const nowMs = params.now();
|
|
1672
|
+
const outcome = executeReconciliation({
|
|
1673
|
+
fs: params.fs,
|
|
1674
|
+
farmRoot,
|
|
1675
|
+
claudeHome: params.claudeHome,
|
|
1676
|
+
actions: reconciliation.actions,
|
|
1677
|
+
nowMs,
|
|
1678
|
+
classification: params.classification
|
|
1679
|
+
});
|
|
1680
|
+
const facts = buildEntryFacts({
|
|
1681
|
+
fs: params.fs,
|
|
1682
|
+
claudeHome: params.claudeHome,
|
|
1683
|
+
home: params.home,
|
|
1684
|
+
cwd: params.cwd,
|
|
1685
|
+
nowMs,
|
|
1686
|
+
env: params.env,
|
|
1687
|
+
...params.branch === void 0 ? {} : { branch: params.branch },
|
|
1688
|
+
...params.branchDetached === void 0 ? {} : { branchDetached: params.branchDetached }
|
|
1689
|
+
});
|
|
1690
|
+
const resolved = resolveDecisions({ facts, cascade: params.cascade, classification: params.classification });
|
|
1691
|
+
const manifest = {
|
|
1692
|
+
version: 1,
|
|
1693
|
+
builtAtMs: nowMs,
|
|
1694
|
+
identity: params.identity,
|
|
1695
|
+
...params.configProfile === void 0 ? {} : { configProfile: params.configProfile },
|
|
1696
|
+
cwd: params.cwd,
|
|
1697
|
+
claudeHome: params.claudeHome,
|
|
1698
|
+
materialised: resolved.farm.materialised,
|
|
1699
|
+
links: resolved.farm.links
|
|
1700
|
+
};
|
|
1701
|
+
const diagnostics = [
|
|
1702
|
+
...resolved.diagnostics,
|
|
1703
|
+
...reconciliation.diagnostics,
|
|
1704
|
+
...outcome.diagnostics,
|
|
1705
|
+
...recoveryDiagnostics(recovery, params.identity)
|
|
1706
|
+
];
|
|
1707
|
+
const farmExists = params.fs.lstat(farmRoot) !== void 0;
|
|
1708
|
+
const nothingAdopted = outcome.adopted.length === 0 && outcome.conflicts.length === 0;
|
|
1709
|
+
if (farmExists && !recovery.recovered && nothingAdopted && previousManifest !== void 0 && farmAlreadyMatches(previousManifest, manifest)) {
|
|
1710
|
+
return {
|
|
1711
|
+
farmRoot,
|
|
1712
|
+
noOp: true,
|
|
1713
|
+
manifest: previousManifest,
|
|
1714
|
+
resolved,
|
|
1715
|
+
adopted: outcome.adopted,
|
|
1716
|
+
recovery,
|
|
1717
|
+
diagnostics
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
const scratchRoot = path4.join(params.identitiesDir, `.${params.identity}.scratch.${params.uniqueSuffix}`);
|
|
1721
|
+
const previousRoot = path4.join(params.identitiesDir, `.${params.identity}.previous.${params.uniqueSuffix}`);
|
|
1722
|
+
buildScratchTree({ fs: params.fs, scratchRoot, plan: resolved.farm, manifest });
|
|
1723
|
+
const swap = swapIn({ fs: params.fs, farmRoot, scratchRoot, previousRoot, classification: params.classification });
|
|
1724
|
+
if (swap.retainedPrevious !== void 0) {
|
|
1725
|
+
diagnostics.push({
|
|
1726
|
+
code: "FARM_PREVIOUS_RETAINED",
|
|
1727
|
+
severity: "warning",
|
|
1728
|
+
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 \`agent-shim identity resolve-conflicts ${params.identity}\` to resolve it interactively.`,
|
|
1729
|
+
subject: swap.retainedPrevious
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
return {
|
|
1733
|
+
farmRoot,
|
|
1734
|
+
noOp: false,
|
|
1735
|
+
manifest,
|
|
1736
|
+
resolved,
|
|
1737
|
+
adopted: outcome.adopted,
|
|
1738
|
+
recovery,
|
|
1739
|
+
diagnostics,
|
|
1740
|
+
...swap.retainedPrevious === void 0 ? {} : { retainedPrevious: swap.retainedPrevious }
|
|
1741
|
+
};
|
|
1742
|
+
} finally {
|
|
1743
|
+
lock.release();
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
function recoveryDiagnostics(recovery, identity) {
|
|
1747
|
+
if (!recovery.recovered) {
|
|
1748
|
+
return [];
|
|
1749
|
+
}
|
|
1750
|
+
const parts = [];
|
|
1751
|
+
if (recovery.removedScratch.length > 0) {
|
|
1752
|
+
parts.push(`removed ${String(recovery.removedScratch.length)} abandoned scratch tree(s)`);
|
|
1753
|
+
}
|
|
1754
|
+
if (recovery.restoredFrom !== void 0) {
|
|
1755
|
+
parts.push(`restored the farm from ${recovery.restoredFrom}, which a previous launch was killed mid-swap`);
|
|
1756
|
+
}
|
|
1757
|
+
if (recovery.completed.length > 0) {
|
|
1758
|
+
parts.push(`finished carrying local state out of ${String(recovery.completed.length)} superseded farm(s)`);
|
|
1759
|
+
}
|
|
1760
|
+
if (recovery.autoResolved.length > 0) {
|
|
1761
|
+
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`);
|
|
1762
|
+
}
|
|
1763
|
+
if (recovery.retained.length > 0) {
|
|
1764
|
+
parts.push(
|
|
1765
|
+
`kept ${recovery.retained.join(", ")}, which still holds data the current farm also has an entry for \u2014 run \`agent-shim identity resolve-conflicts ${identity}\` to resolve it interactively`
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
return [
|
|
1769
|
+
{
|
|
1770
|
+
code: "FARM_SWAP_RECOVERED",
|
|
1771
|
+
severity: "warning",
|
|
1772
|
+
message: `A previous launch left this identity's farm mid-swap. This launch ${parts.join("; ")}.`
|
|
1773
|
+
}
|
|
1774
|
+
];
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/frontdoor/connect.ts
|
|
1778
|
+
import { X509Certificate as X509Certificate2, createPrivateKey as createPrivateKey2 } from "node:crypto";
|
|
1779
|
+
import * as fs from "node:fs";
|
|
1780
|
+
import * as http from "node:http";
|
|
1781
|
+
import * as https from "node:https";
|
|
1782
|
+
import * as net2 from "node:net";
|
|
1783
|
+
import * as tls from "node:tls";
|
|
1784
|
+
|
|
1785
|
+
// src/frontdoor/x509.ts
|
|
1786
|
+
import { createHash, createPrivateKey, generateKeyPairSync, randomBytes, sign, X509Certificate } from "node:crypto";
|
|
1787
|
+
import net from "node:net";
|
|
1788
|
+
var TAG_BOOLEAN = 1;
|
|
1789
|
+
var TAG_INTEGER = 2;
|
|
1790
|
+
var TAG_BIT_STRING = 3;
|
|
1791
|
+
var TAG_OCTET_STRING = 4;
|
|
1792
|
+
var TAG_NULL = 5;
|
|
1793
|
+
var TAG_OID = 6;
|
|
1794
|
+
var TAG_UTF8_STRING = 12;
|
|
1795
|
+
var TAG_UTC_TIME = 23;
|
|
1796
|
+
var TAG_GENERALIZED_TIME = 24;
|
|
1797
|
+
var TAG_SEQUENCE = 48;
|
|
1798
|
+
var TAG_SET = 49;
|
|
1799
|
+
var TAG_VERSION = 160;
|
|
1800
|
+
var TAG_EXTENSIONS = 163;
|
|
1801
|
+
var TAG_GENERAL_NAME_DNS = 130;
|
|
1802
|
+
var TAG_GENERAL_NAME_IP = 135;
|
|
1803
|
+
var TAG_KEY_IDENTIFIER = 128;
|
|
1804
|
+
var OID_COMMON_NAME = "2.5.4.3";
|
|
1805
|
+
var OID_SHA256_WITH_RSA = "1.2.840.113549.1.1.11";
|
|
1806
|
+
var OID_SUBJECT_KEY_IDENTIFIER = "2.5.29.14";
|
|
1807
|
+
var OID_KEY_USAGE = "2.5.29.15";
|
|
1808
|
+
var OID_SUBJECT_ALT_NAME = "2.5.29.17";
|
|
1809
|
+
var OID_BASIC_CONSTRAINTS = "2.5.29.19";
|
|
1810
|
+
var OID_AUTHORITY_KEY_IDENTIFIER = "2.5.29.35";
|
|
1811
|
+
var OID_EXT_KEY_USAGE = "2.5.29.37";
|
|
1812
|
+
var OID_SERVER_AUTH = "1.3.6.1.5.5.7.3.1";
|
|
1813
|
+
var VERSION_3 = 2;
|
|
1814
|
+
var SERIAL_NUMBER_BYTES = 16;
|
|
1815
|
+
var RSA_MODULUS_BITS = 2048;
|
|
1816
|
+
var BITS_PER_BYTE = 8;
|
|
1817
|
+
var BYTE_MASK = 255;
|
|
1818
|
+
var HIGH_BIT = 128;
|
|
1819
|
+
var BASE_128_PAYLOAD_MASK = 127;
|
|
1820
|
+
var FIRST_ARC_WEIGHT = 40;
|
|
1821
|
+
var SHORT_FORM_MAX_LENGTH = 127;
|
|
1822
|
+
var MS_PER_SECOND = 1e3;
|
|
1823
|
+
var UTC_TIME_LAST_YEAR = 2049;
|
|
1824
|
+
var YEAR_DIGITS = 4;
|
|
1825
|
+
var TWO_DIGIT_YEAR_DIGITS = 2;
|
|
1826
|
+
var COMPACT_TIME_DIGITS = 14;
|
|
1827
|
+
var KEY_USAGE = { digitalSignature: 0, keyEncipherment: 2, keyCertSign: 5, cRLSign: 6 };
|
|
1828
|
+
function tlv(tag, content) {
|
|
1829
|
+
const length = content.length;
|
|
1830
|
+
if (length <= SHORT_FORM_MAX_LENGTH) {
|
|
1831
|
+
return Buffer.concat([Buffer.from([tag, length]), content]);
|
|
1832
|
+
}
|
|
1833
|
+
const lengthBytes = [];
|
|
1834
|
+
for (let remaining = length; remaining > 0; remaining = Math.floor(remaining / (BYTE_MASK + 1))) {
|
|
1835
|
+
lengthBytes.unshift(remaining & BYTE_MASK);
|
|
1836
|
+
}
|
|
1837
|
+
return Buffer.concat([Buffer.from([tag, HIGH_BIT | lengthBytes.length, ...lengthBytes]), content]);
|
|
1838
|
+
}
|
|
1839
|
+
function sequence(...parts) {
|
|
1840
|
+
return tlv(TAG_SEQUENCE, Buffer.concat(parts));
|
|
1841
|
+
}
|
|
1842
|
+
function unsignedInteger(magnitude) {
|
|
1843
|
+
let start = 0;
|
|
1844
|
+
while (start < magnitude.length - 1 && magnitude[start] === 0) {
|
|
1845
|
+
start += 1;
|
|
1846
|
+
}
|
|
1847
|
+
const trimmed = magnitude.subarray(start);
|
|
1848
|
+
const first = trimmed[0];
|
|
1849
|
+
const padded = first !== void 0 && (first & HIGH_BIT) !== 0 ? Buffer.concat([Buffer.from([0]), trimmed]) : Buffer.from(trimmed);
|
|
1850
|
+
return tlv(TAG_INTEGER, padded);
|
|
1851
|
+
}
|
|
1852
|
+
function smallInteger(value) {
|
|
1853
|
+
return unsignedInteger(Buffer.from([value]));
|
|
1854
|
+
}
|
|
1855
|
+
function oid(dotted) {
|
|
1856
|
+
const arcs = dotted.split(".").map(Number);
|
|
1857
|
+
const [first, second, ...rest] = arcs;
|
|
1858
|
+
if (first === void 0 || second === void 0 || arcs.some((arc) => !Number.isInteger(arc) || arc < 0)) {
|
|
1859
|
+
throw new Error(`invalid object identifier ${dotted}`);
|
|
1860
|
+
}
|
|
1861
|
+
const encoded = [];
|
|
1862
|
+
for (const arc of [first * FIRST_ARC_WEIGHT + second, ...rest]) {
|
|
1863
|
+
const groups = [arc & BASE_128_PAYLOAD_MASK];
|
|
1864
|
+
for (let remaining = Math.floor(arc / HIGH_BIT); remaining > 0; remaining = Math.floor(remaining / HIGH_BIT)) {
|
|
1865
|
+
groups.unshift(remaining & BASE_128_PAYLOAD_MASK | HIGH_BIT);
|
|
1866
|
+
}
|
|
1867
|
+
encoded.push(...groups);
|
|
1868
|
+
}
|
|
1869
|
+
return tlv(TAG_OID, Buffer.from(encoded));
|
|
1870
|
+
}
|
|
1871
|
+
function boolean(value) {
|
|
1872
|
+
return tlv(TAG_BOOLEAN, Buffer.from([value ? BYTE_MASK : 0]));
|
|
1873
|
+
}
|
|
1874
|
+
function octetString(content) {
|
|
1875
|
+
return tlv(TAG_OCTET_STRING, content);
|
|
1876
|
+
}
|
|
1877
|
+
function wholeBitString(content) {
|
|
1878
|
+
return tlv(TAG_BIT_STRING, Buffer.concat([Buffer.from([0]), content]));
|
|
1879
|
+
}
|
|
1880
|
+
function time(instant) {
|
|
1881
|
+
const iso = new Date(Math.floor(instant.getTime() / MS_PER_SECOND) * MS_PER_SECOND).toISOString();
|
|
1882
|
+
const compact = `${iso.replace(/[-:T]/g, "").slice(0, COMPACT_TIME_DIGITS)}Z`;
|
|
1883
|
+
if (instant.getUTCFullYear() > UTC_TIME_LAST_YEAR) {
|
|
1884
|
+
return tlv(TAG_GENERALIZED_TIME, Buffer.from(compact, "ascii"));
|
|
1885
|
+
}
|
|
1886
|
+
return tlv(TAG_UTC_TIME, Buffer.from(compact.slice(YEAR_DIGITS - TWO_DIGIT_YEAR_DIGITS), "ascii"));
|
|
1887
|
+
}
|
|
1888
|
+
function commonNameOnly(commonName) {
|
|
1889
|
+
return sequence(tlv(TAG_SET, sequence(oid(OID_COMMON_NAME), tlv(TAG_UTF8_STRING, Buffer.from(commonName, "utf8")))));
|
|
1890
|
+
}
|
|
1891
|
+
var SIGNATURE_ALGORITHM = sequence(oid(OID_SHA256_WITH_RSA), tlv(TAG_NULL, Buffer.alloc(0)));
|
|
1892
|
+
function extension(extensionOid, critical, value) {
|
|
1893
|
+
return sequence(oid(extensionOid), ...critical ? [boolean(true)] : [], octetString(value));
|
|
1894
|
+
}
|
|
1895
|
+
function keyUsageExtension(bits) {
|
|
1896
|
+
const highest = Math.max(...bits);
|
|
1897
|
+
const byteCount = Math.floor(highest / BITS_PER_BYTE) + 1;
|
|
1898
|
+
const bytes = Buffer.alloc(byteCount);
|
|
1899
|
+
for (const bit of bits) {
|
|
1900
|
+
const index = Math.floor(bit / BITS_PER_BYTE);
|
|
1901
|
+
bytes[index] = (bytes[index] ?? 0) | HIGH_BIT >> bit % BITS_PER_BYTE;
|
|
1902
|
+
}
|
|
1903
|
+
const unusedBits = BITS_PER_BYTE - 1 - highest % BITS_PER_BYTE;
|
|
1904
|
+
return extension(OID_KEY_USAGE, true, tlv(TAG_BIT_STRING, Buffer.concat([Buffer.from([unusedBits]), bytes])));
|
|
1905
|
+
}
|
|
1906
|
+
var IPV6_GROUPS = 8;
|
|
1907
|
+
var HEX_RADIX = 16;
|
|
1908
|
+
var BYTES_PER_IPV6_GROUP = 2;
|
|
1909
|
+
function ipv6Groups(address) {
|
|
1910
|
+
const [head = "", tail] = address.split("::");
|
|
1911
|
+
const split = (text) => text === "" ? [] : text.split(":");
|
|
1912
|
+
const expand = (groups) => groups.flatMap((group) => {
|
|
1913
|
+
if (!net.isIPv4(group)) {
|
|
1914
|
+
return [Number.parseInt(group, HEX_RADIX)];
|
|
1915
|
+
}
|
|
1916
|
+
const [a = 0, b = 0, c = 0, d = 0] = group.split(".").map(Number);
|
|
1917
|
+
return [a * (BYTE_MASK + 1) + b, c * (BYTE_MASK + 1) + d];
|
|
1918
|
+
});
|
|
1919
|
+
const before = expand(split(head));
|
|
1920
|
+
const after = tail === void 0 ? [] : expand(split(tail));
|
|
1921
|
+
const filler = tail === void 0 ? [] : Array.from({ length: IPV6_GROUPS - before.length - after.length }, () => 0);
|
|
1922
|
+
return [...before, ...filler, ...after];
|
|
1923
|
+
}
|
|
1924
|
+
function ipBytes(address) {
|
|
1925
|
+
if (net.isIPv4(address)) {
|
|
1926
|
+
return Buffer.from(address.split(".").map(Number));
|
|
1927
|
+
}
|
|
1928
|
+
if (!net.isIPv6(address)) {
|
|
1929
|
+
throw new Error(`${address} is not an IP address`);
|
|
1930
|
+
}
|
|
1931
|
+
const bytes = Buffer.alloc(IPV6_GROUPS * BYTES_PER_IPV6_GROUP);
|
|
1932
|
+
ipv6Groups(address).forEach((group, index) => {
|
|
1933
|
+
bytes.writeUInt16BE(group, index * BYTES_PER_IPV6_GROUP);
|
|
1934
|
+
});
|
|
1935
|
+
return bytes;
|
|
1936
|
+
}
|
|
1937
|
+
function keyIdentifier(publicKey) {
|
|
1938
|
+
return createHash("sha1").update(publicKey.export({ type: "pkcs1", format: "der" })).digest();
|
|
1939
|
+
}
|
|
1940
|
+
function readElement(buffer, offset) {
|
|
1941
|
+
const tag = buffer[offset];
|
|
1942
|
+
const first = buffer[offset + 1];
|
|
1943
|
+
if (tag === void 0 || first === void 0) {
|
|
1944
|
+
throw new Error("truncated DER");
|
|
1945
|
+
}
|
|
1946
|
+
let length = first;
|
|
1947
|
+
let contentStart = offset + 2;
|
|
1948
|
+
if ((first & HIGH_BIT) !== 0) {
|
|
1949
|
+
const count = first & BASE_128_PAYLOAD_MASK;
|
|
1950
|
+
length = 0;
|
|
1951
|
+
for (let index = 0; index < count; index += 1) {
|
|
1952
|
+
length = length * (BYTE_MASK + 1) + (buffer[offset + 2 + index] ?? 0);
|
|
1953
|
+
}
|
|
1954
|
+
contentStart = offset + 2 + count;
|
|
1955
|
+
}
|
|
1956
|
+
return { tag, start: offset, contentStart, end: contentStart + length };
|
|
1957
|
+
}
|
|
1958
|
+
function subjectNameOf(certificateDer) {
|
|
1959
|
+
const certificate = readElement(certificateDer, 0);
|
|
1960
|
+
let cursor = readElement(certificateDer, certificate.contentStart);
|
|
1961
|
+
const tbsEnd = cursor.end;
|
|
1962
|
+
cursor = readElement(certificateDer, cursor.contentStart);
|
|
1963
|
+
if (cursor.tag === TAG_VERSION) {
|
|
1964
|
+
cursor = readElement(certificateDer, cursor.end);
|
|
1965
|
+
}
|
|
1966
|
+
const fieldsBeforeSubject = 4;
|
|
1967
|
+
for (let skipped = 1; skipped < fieldsBeforeSubject; skipped += 1) {
|
|
1968
|
+
cursor = readElement(certificateDer, cursor.end);
|
|
1969
|
+
}
|
|
1970
|
+
const subject = readElement(certificateDer, cursor.end);
|
|
1971
|
+
if (subject.end > tbsEnd) {
|
|
1972
|
+
throw new Error("certificate has no subject");
|
|
1973
|
+
}
|
|
1974
|
+
return Buffer.from(certificateDer.subarray(subject.start, subject.end));
|
|
1975
|
+
}
|
|
1976
|
+
function toPem(label, der) {
|
|
1977
|
+
const body = Buffer.from(der).toString("base64").match(/.{1,64}/g) ?? [];
|
|
1978
|
+
return `-----BEGIN ${label}-----
|
|
1979
|
+
${body.join("\n")}
|
|
1980
|
+
-----END ${label}-----
|
|
1981
|
+
`;
|
|
1982
|
+
}
|
|
1983
|
+
function randomSerial() {
|
|
1984
|
+
const bytes = randomBytes(SERIAL_NUMBER_BYTES);
|
|
1985
|
+
bytes[0] = (bytes[0] ?? 0) & BASE_128_PAYLOAD_MASK | HIGH_BIT >> 1;
|
|
1986
|
+
return bytes;
|
|
1987
|
+
}
|
|
1988
|
+
function assemble(spec) {
|
|
1989
|
+
const tbs = sequence(
|
|
1990
|
+
tlv(TAG_VERSION, smallInteger(VERSION_3)),
|
|
1991
|
+
unsignedInteger(randomSerial()),
|
|
1992
|
+
SIGNATURE_ALGORITHM,
|
|
1993
|
+
spec.issuerName,
|
|
1994
|
+
sequence(time(spec.notBefore), time(spec.notAfter)),
|
|
1995
|
+
spec.subjectName,
|
|
1996
|
+
spec.subjectPublicKey.export({ type: "spki", format: "der" }),
|
|
1997
|
+
tlv(TAG_EXTENSIONS, sequence(...spec.extensions))
|
|
1998
|
+
);
|
|
1999
|
+
const signature = sign("sha256", tbs, spec.signingKey);
|
|
2000
|
+
return sequence(tbs, SIGNATURE_ALGORITHM, wholeBitString(signature));
|
|
2001
|
+
}
|
|
2002
|
+
function subjectKeyIdentifierExtension(publicKey) {
|
|
2003
|
+
return extension(OID_SUBJECT_KEY_IDENTIFIER, false, octetString(keyIdentifier(publicKey)));
|
|
2004
|
+
}
|
|
2005
|
+
function basicConstraintsExtension(isCa) {
|
|
2006
|
+
return extension(OID_BASIC_CONSTRAINTS, true, sequence(...isCa ? [boolean(true)] : []));
|
|
2007
|
+
}
|
|
2008
|
+
function rsaKeyPair() {
|
|
2009
|
+
return generateKeyPairSync("rsa", { modulusLength: RSA_MODULUS_BITS });
|
|
2010
|
+
}
|
|
2011
|
+
function keyPem(privateKey) {
|
|
2012
|
+
return privateKey.export({ type: "pkcs8", format: "pem" });
|
|
2013
|
+
}
|
|
2014
|
+
function issueCaCertificate(commonName, notBefore, notAfter) {
|
|
2015
|
+
const { publicKey, privateKey } = rsaKeyPair();
|
|
2016
|
+
const name = commonNameOnly(commonName);
|
|
2017
|
+
const der = assemble({
|
|
2018
|
+
subjectPublicKey: publicKey,
|
|
2019
|
+
subjectName: name,
|
|
2020
|
+
issuerName: name,
|
|
2021
|
+
signingKey: privateKey,
|
|
2022
|
+
notBefore,
|
|
2023
|
+
notAfter,
|
|
2024
|
+
extensions: [basicConstraintsExtension(true), keyUsageExtension([KEY_USAGE.keyCertSign, KEY_USAGE.cRLSign]), subjectKeyIdentifierExtension(publicKey)]
|
|
2025
|
+
});
|
|
2026
|
+
return { certPem: toPem("CERTIFICATE", der), keyPem: keyPem(privateKey) };
|
|
2027
|
+
}
|
|
2028
|
+
function issueLeafCertificate(ca, names, notBefore, notAfter) {
|
|
2029
|
+
const [commonName] = names;
|
|
2030
|
+
if (commonName === void 0) {
|
|
2031
|
+
throw new Error("a leaf certificate needs at least one name");
|
|
2032
|
+
}
|
|
2033
|
+
const caCertificate = new X509Certificate(ca.certPem);
|
|
2034
|
+
const caPrivateKey = createPrivateKey(ca.keyPem);
|
|
2035
|
+
const { publicKey, privateKey } = rsaKeyPair();
|
|
2036
|
+
const generalNames = names.map((name) => net.isIP(name) === 0 ? tlv(TAG_GENERAL_NAME_DNS, Buffer.from(name, "ascii")) : tlv(TAG_GENERAL_NAME_IP, ipBytes(name)));
|
|
2037
|
+
const der = assemble({
|
|
2038
|
+
subjectPublicKey: publicKey,
|
|
2039
|
+
subjectName: commonNameOnly(commonName),
|
|
2040
|
+
issuerName: subjectNameOf(caCertificate.raw),
|
|
2041
|
+
signingKey: caPrivateKey,
|
|
2042
|
+
notBefore,
|
|
2043
|
+
notAfter,
|
|
2044
|
+
extensions: [
|
|
2045
|
+
basicConstraintsExtension(false),
|
|
2046
|
+
keyUsageExtension([KEY_USAGE.digitalSignature, KEY_USAGE.keyEncipherment]),
|
|
2047
|
+
extension(OID_EXT_KEY_USAGE, false, sequence(oid(OID_SERVER_AUTH))),
|
|
2048
|
+
extension(OID_SUBJECT_ALT_NAME, false, sequence(...generalNames)),
|
|
2049
|
+
subjectKeyIdentifierExtension(publicKey),
|
|
2050
|
+
extension(OID_AUTHORITY_KEY_IDENTIFIER, false, sequence(tlv(TAG_KEY_IDENTIFIER, keyIdentifier(caCertificate.publicKey))))
|
|
2051
|
+
]
|
|
2052
|
+
});
|
|
2053
|
+
return { certPem: toPem("CERTIFICATE", der), keyPem: keyPem(privateKey) };
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/frontdoor/capability.ts
|
|
2057
|
+
var CONNECT_PROXY_REALM = "agent-shim front door";
|
|
2058
|
+
var BASIC_SCHEME = "basic";
|
|
2059
|
+
function capabilityFromProxyAuthorization(value) {
|
|
2060
|
+
const trimmed = value.trim();
|
|
2061
|
+
const space = trimmed.indexOf(" ");
|
|
2062
|
+
if (space === -1 || trimmed.slice(0, space).toLowerCase() !== BASIC_SCHEME) {
|
|
2063
|
+
return void 0;
|
|
2064
|
+
}
|
|
2065
|
+
const userPass = Buffer.from(trimmed.slice(space + 1).trim(), "base64").toString("utf8");
|
|
2066
|
+
const colon = userPass.indexOf(":");
|
|
2067
|
+
if (colon === -1) {
|
|
2068
|
+
return void 0;
|
|
2069
|
+
}
|
|
2070
|
+
const password = userPass.slice(colon + 1);
|
|
2071
|
+
return password === "" ? void 0 : password;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/frontdoor/state.ts
|
|
2075
|
+
import path5 from "node:path";
|
|
2076
|
+
import { z as z3 } from "zod";
|
|
2077
|
+
var FrontDoorSessionSchema = z3.strictObject({ pid: z3.number().int().positive(), startedAt: z3.number(), token: z3.string().min(1) });
|
|
2078
|
+
var FrontDoorStateSchema = z3.strictObject({
|
|
2079
|
+
/** The supervisor process serving the front door. Alive means both listeners are up in that process. */
|
|
2080
|
+
supervisorPid: z3.number().int().positive().optional(),
|
|
2081
|
+
/** 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. */
|
|
2082
|
+
port: z3.number().int().positive().optional(),
|
|
2083
|
+
/**
|
|
2084
|
+
* 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.
|
|
2085
|
+
*/
|
|
2086
|
+
lastPort: z3.number().int().positive().optional(),
|
|
2087
|
+
/**
|
|
2088
|
+
* 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`.
|
|
2089
|
+
*/
|
|
2090
|
+
connectPort: z3.number().int().positive().optional(),
|
|
2091
|
+
/**
|
|
2092
|
+
* 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.
|
|
2093
|
+
*/
|
|
2094
|
+
lastConnectPort: z3.number().int().positive().optional(),
|
|
2095
|
+
/**
|
|
2096
|
+
* 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.
|
|
2097
|
+
*/
|
|
2098
|
+
directPort: z3.number().int().positive().optional(),
|
|
2099
|
+
/** The sticky port preference for the direct listener, for the reason `directPort` gives. */
|
|
2100
|
+
lastDirectPort: z3.number().int().positive().optional(),
|
|
2101
|
+
/** The last fatal error, kept across shutdowns so `frontdoor status` can explain a front door that is not running. */
|
|
2102
|
+
lastError: z3.string().optional()
|
|
2103
|
+
});
|
|
2104
|
+
function readFrontDoorState(fs2, stateFile) {
|
|
2105
|
+
let raw;
|
|
2106
|
+
try {
|
|
2107
|
+
raw = fs2.readFileUtf8(stateFile);
|
|
2108
|
+
} catch {
|
|
2109
|
+
return void 0;
|
|
2110
|
+
}
|
|
2111
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
2112
|
+
return void 0;
|
|
2113
|
+
}
|
|
2114
|
+
try {
|
|
2115
|
+
const parsed = FrontDoorStateSchema.safeParse(JSON.parse(raw));
|
|
2116
|
+
return parsed.success ? parsed.data : void 0;
|
|
2117
|
+
} catch {
|
|
2118
|
+
return void 0;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
function writeFrontDoorState(fs2, stateFile, state) {
|
|
2122
|
+
fs2.mkdirp(path5.dirname(stateFile));
|
|
2123
|
+
fs2.writeFileUtf8(stateFile, `${JSON.stringify(state, null, 2)}
|
|
2124
|
+
`);
|
|
2125
|
+
}
|
|
2126
|
+
function frontDoorOrigin(state) {
|
|
2127
|
+
const port = state?.directPort ?? state?.lastDirectPort;
|
|
2128
|
+
return port === void 0 ? void 0 : `http://127.0.0.1:${String(port)}`;
|
|
2129
|
+
}
|
|
2130
|
+
function writeFrontDoorSession(fs2, sessionsDir, session) {
|
|
2131
|
+
fs2.mkdirPrivate(sessionsDir);
|
|
2132
|
+
fs2.writeFilePrivate(path5.join(sessionsDir, `${String(session.pid)}.json`), `${JSON.stringify(session, null, 2)}
|
|
2133
|
+
`);
|
|
2134
|
+
}
|
|
2135
|
+
function listFrontDoorSessions(fs2, sessionsDir) {
|
|
2136
|
+
const sessions = [];
|
|
2137
|
+
for (const name of fs2.readdir(sessionsDir)) {
|
|
2138
|
+
if (!name.endsWith(".json")) {
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
try {
|
|
2142
|
+
const parsed = FrontDoorSessionSchema.safeParse(JSON.parse(fs2.readFileUtf8(path5.join(sessionsDir, name)) ?? ""));
|
|
2143
|
+
if (parsed.success) {
|
|
2144
|
+
sessions.push({ pid: parsed.data.pid, startedAt: parsed.data.startedAt });
|
|
2145
|
+
}
|
|
2146
|
+
} catch {
|
|
2147
|
+
continue;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
return sessions.sort((a, b) => a.pid - b.pid);
|
|
2151
|
+
}
|
|
2152
|
+
function removeFrontDoorSession(fs2, sessionsDir, pid) {
|
|
2153
|
+
fs2.removeRecursive(path5.join(sessionsDir, `${String(pid)}.json`));
|
|
2154
|
+
}
|
|
2155
|
+
function pruneDeadFrontDoorSessions(fs2, sessionsDir, isRunning) {
|
|
2156
|
+
const removed = [];
|
|
2157
|
+
for (const session of listFrontDoorSessions(fs2, sessionsDir)) {
|
|
2158
|
+
if (!isRunning(session.pid)) {
|
|
2159
|
+
removeFrontDoorSession(fs2, sessionsDir, session.pid);
|
|
2160
|
+
removed.push(session.pid);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return removed;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// src/frontdoor/supervisor.ts
|
|
2167
|
+
var FRONTDOOR_SUPERVISOR_STILL_RUNNING = -1;
|
|
2168
|
+
var FRONTDOOR_POLL_MS = 1e3;
|
|
2169
|
+
var MS_PER_MINUTE = 6e4;
|
|
2170
|
+
async function runFrontDoorSupervisor(idleShutdownMinutes, ports, options = {}) {
|
|
2171
|
+
const { fs: fs2, paths } = ports;
|
|
2172
|
+
const previous = readFrontDoorState(fs2, paths.frontdoorStateFile);
|
|
2173
|
+
let lastPort = previous?.lastPort;
|
|
2174
|
+
let lastConnectPort = previous?.lastConnectPort;
|
|
2175
|
+
let lastDirectPort = previous?.lastDirectPort;
|
|
2176
|
+
const sticky = () => ({
|
|
2177
|
+
...lastPort === void 0 ? {} : { lastPort },
|
|
2178
|
+
...lastConnectPort === void 0 ? {} : { lastConnectPort },
|
|
2179
|
+
...lastDirectPort === void 0 ? {} : { lastDirectPort }
|
|
2180
|
+
});
|
|
2181
|
+
const fail = (message) => {
|
|
2182
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { lastError: message, ...sticky() });
|
|
2183
|
+
fs2.removeRecursive(paths.frontdoorLockFile);
|
|
2184
|
+
ports.log(`agent-shim frontdoor supervisor: ${message}`);
|
|
2185
|
+
return 1;
|
|
2186
|
+
};
|
|
2187
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, ...sticky() });
|
|
2188
|
+
const previousSticky = lastPort;
|
|
2189
|
+
let http2;
|
|
2190
|
+
try {
|
|
2191
|
+
http2 = await ports.startProviderListener(lastPort);
|
|
2192
|
+
} catch (error) {
|
|
2193
|
+
return fail(`could not start the provider listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
2194
|
+
}
|
|
2195
|
+
if (previousSticky !== void 0 && http2.port !== previousSticky) {
|
|
2196
|
+
ports.log(`agent-shim frontdoor supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(http2.port)} (sessions launched against the old port are stale until they relaunch)`);
|
|
2197
|
+
}
|
|
2198
|
+
lastPort = http2.port;
|
|
2199
|
+
const previousStickyConnect = lastConnectPort;
|
|
2200
|
+
let connect2;
|
|
2201
|
+
try {
|
|
2202
|
+
connect2 = await ports.startConnectListener(lastConnectPort);
|
|
2203
|
+
} catch (error) {
|
|
2204
|
+
await http2.close();
|
|
2205
|
+
return fail(`could not start the CONNECT surface: ${error instanceof Error ? error.message : String(error)}`);
|
|
2206
|
+
}
|
|
2207
|
+
if (previousStickyConnect !== void 0 && connect2.port !== previousStickyConnect) {
|
|
2208
|
+
ports.log(`agent-shim 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)`);
|
|
2209
|
+
}
|
|
2210
|
+
lastConnectPort = connect2.port;
|
|
2211
|
+
const previousStickyDirect = lastDirectPort;
|
|
2212
|
+
let direct;
|
|
2213
|
+
try {
|
|
2214
|
+
direct = await ports.startDirectListener(lastDirectPort);
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
await connect2.close();
|
|
2217
|
+
await http2.close();
|
|
2218
|
+
return fail(`could not start the direct listener: ${error instanceof Error ? error.message : String(error)}`);
|
|
2219
|
+
}
|
|
2220
|
+
if (previousStickyDirect !== void 0 && direct.port !== previousStickyDirect) {
|
|
2221
|
+
ports.log(`agent-shim frontdoor supervisor: sticky direct port ${String(previousStickyDirect)} was occupied; moving to ${String(direct.port)} (headroom must be restarted to admit the new address)`);
|
|
2222
|
+
}
|
|
2223
|
+
lastDirectPort = direct.port;
|
|
2224
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, { supervisorPid: ports.ownPid, port: http2.port, connectPort: connect2.port, directPort: direct.port, ...sticky() });
|
|
2225
|
+
fs2.removeRecursive(paths.frontdoorLockFile);
|
|
2226
|
+
ports.log(`agent-shim 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)}`);
|
|
2227
|
+
let idleSince;
|
|
2228
|
+
let ticks = 0;
|
|
2229
|
+
for (; ; ) {
|
|
2230
|
+
if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
|
|
2231
|
+
return FRONTDOOR_SUPERVISOR_STILL_RUNNING;
|
|
2232
|
+
}
|
|
2233
|
+
ticks += 1;
|
|
2234
|
+
pruneDeadFrontDoorSessions(fs2, paths.frontdoorSessionsDir, ports.isRunning);
|
|
2235
|
+
if (listFrontDoorSessions(fs2, paths.frontdoorSessionsDir).length === 0) {
|
|
2236
|
+
idleSince ??= ports.now();
|
|
2237
|
+
if (ports.now() - idleSince >= idleShutdownMinutes * MS_PER_MINUTE) {
|
|
2238
|
+
ports.log(`agent-shim frontdoor supervisor: no sessions for ${String(idleShutdownMinutes)} minute(s); closing the front door and exiting`);
|
|
2239
|
+
await direct.close();
|
|
2240
|
+
await connect2.close();
|
|
2241
|
+
await http2.close();
|
|
2242
|
+
writeFrontDoorState(fs2, paths.frontdoorStateFile, sticky());
|
|
2243
|
+
return 0;
|
|
2244
|
+
}
|
|
2245
|
+
} else {
|
|
2246
|
+
idleSince = void 0;
|
|
2247
|
+
}
|
|
2248
|
+
await ports.sleep(FRONTDOOR_POLL_MS);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// src/frontdoor/connect.ts
|
|
2253
|
+
var CONNECT_INTERCEPT_HOST = "api.anthropic.com";
|
|
2254
|
+
var ROUTED_PATH_PREFIX = "/v1/";
|
|
2255
|
+
var HTTPS_PORT = 443;
|
|
2256
|
+
var MAX_PORT = 65535;
|
|
2257
|
+
var HTTP_BAD_GATEWAY = 502;
|
|
2258
|
+
var CA_KEY_FILE_MODE = 384;
|
|
2259
|
+
var CONNECT_LINE_TOKENS = 3;
|
|
2260
|
+
var HEAD_TERMINATOR = "\r\n\r\n";
|
|
2261
|
+
var MAX_CONNECT_HEAD_BYTES = 8192;
|
|
2262
|
+
var CONNECT_ESTABLISHED = "HTTP/1.1 200 Connection Established\r\n\r\n";
|
|
2263
|
+
var REFUSAL = {
|
|
2264
|
+
badRequest: "400 Bad Request",
|
|
2265
|
+
proxyAuthenticationRequired: "407 Proxy Authentication Required",
|
|
2266
|
+
requestTimeout: "408 Request Timeout",
|
|
2267
|
+
headTooLarge: "431 Request Header Fields Too Large",
|
|
2268
|
+
serviceUnavailable: "503 Service Unavailable"
|
|
2269
|
+
};
|
|
2270
|
+
var CONNECT_LIMITS = {
|
|
2271
|
+
headDeadlineMs: 1e4,
|
|
2272
|
+
maxPendingHeads: 64,
|
|
2273
|
+
maxTunnels: 1024,
|
|
2274
|
+
revalidateMs: FRONTDOOR_POLL_MS
|
|
2275
|
+
};
|
|
2276
|
+
var CA_VALIDITY_DAYS = 3650;
|
|
2277
|
+
var LEAF_VALIDITY_DAYS = 397;
|
|
2278
|
+
var CA_SUBJECT_COMMON_NAME = "agent-shim front door CA";
|
|
2279
|
+
var MS_PER_DAY = 864e5;
|
|
2280
|
+
function parseConnectTarget(authority) {
|
|
2281
|
+
const colon = authority.lastIndexOf(":");
|
|
2282
|
+
if (colon === -1) {
|
|
2283
|
+
const host2 = authority.trim().toLowerCase();
|
|
2284
|
+
return host2 === "" ? void 0 : { host: host2, port: HTTPS_PORT };
|
|
2285
|
+
}
|
|
2286
|
+
const host = authority.slice(0, colon).trim().toLowerCase();
|
|
2287
|
+
const port = Number.parseInt(authority.slice(colon + 1), 10);
|
|
2288
|
+
if (host === "" || !Number.isInteger(port) || port <= 0 || port > MAX_PORT) {
|
|
2289
|
+
return void 0;
|
|
2290
|
+
}
|
|
2291
|
+
return { host, port };
|
|
2292
|
+
}
|
|
2293
|
+
function isInterceptedHost(host, interceptHost) {
|
|
2294
|
+
return host === interceptHost;
|
|
2295
|
+
}
|
|
2296
|
+
function servedByPipeline(path8) {
|
|
2297
|
+
return path8?.startsWith(ROUTED_PATH_PREFIX) === true;
|
|
2298
|
+
}
|
|
2299
|
+
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
2300
|
+
"connection",
|
|
2301
|
+
"keep-alive",
|
|
2302
|
+
"proxy-connection",
|
|
2303
|
+
"proxy-authenticate",
|
|
2304
|
+
"proxy-authorization",
|
|
2305
|
+
"te",
|
|
2306
|
+
"trailer",
|
|
2307
|
+
"transfer-encoding",
|
|
2308
|
+
"upgrade"
|
|
2309
|
+
]);
|
|
2310
|
+
function forwardableHeaders(headers) {
|
|
2311
|
+
const connectionTokens = new Set(
|
|
2312
|
+
(headers.connection ?? "").split(",").map((token) => token.trim().toLowerCase()).filter((token) => token !== "")
|
|
2313
|
+
);
|
|
2314
|
+
const result = {};
|
|
2315
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
2316
|
+
if (value === void 0) {
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
const lower = name.toLowerCase();
|
|
2320
|
+
if (HOP_BY_HOP_HEADERS.has(lower) || connectionTokens.has(lower)) {
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2323
|
+
result[name] = value;
|
|
2324
|
+
}
|
|
2325
|
+
return result;
|
|
2326
|
+
}
|
|
2327
|
+
function generateCa(now) {
|
|
2328
|
+
return issueCaCertificate(CA_SUBJECT_COMMON_NAME, new Date(now.getTime() - MS_PER_DAY), new Date(now.getTime() + CA_VALIDITY_DAYS * MS_PER_DAY));
|
|
2329
|
+
}
|
|
2330
|
+
function mintLeaf(ca, names, now) {
|
|
2331
|
+
return issueLeafCertificate(ca, names, new Date(now.getTime() - MS_PER_DAY), new Date(now.getTime() + LEAF_VALIDITY_DAYS * MS_PER_DAY));
|
|
2332
|
+
}
|
|
2333
|
+
function createLeafCache(ca, now) {
|
|
2334
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2335
|
+
return (host) => {
|
|
2336
|
+
const existing = cache.get(host);
|
|
2337
|
+
if (existing !== void 0) {
|
|
2338
|
+
return existing;
|
|
2339
|
+
}
|
|
2340
|
+
const minted = mintLeaf(ca, [host], now());
|
|
2341
|
+
cache.set(host, minted);
|
|
2342
|
+
return minted;
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
function ensureCa(store, generate) {
|
|
2346
|
+
const existing = store.loadCa();
|
|
2347
|
+
if (existing !== void 0 && caParses(existing)) {
|
|
2348
|
+
return existing;
|
|
2349
|
+
}
|
|
2350
|
+
const ca = generate();
|
|
2351
|
+
store.writeCa(ca);
|
|
2352
|
+
return ca;
|
|
2353
|
+
}
|
|
2354
|
+
function caParses(ca) {
|
|
2355
|
+
try {
|
|
2356
|
+
new X509Certificate2(ca.certPem);
|
|
2357
|
+
createPrivateKey2(ca.keyPem);
|
|
2358
|
+
return true;
|
|
2359
|
+
} catch {
|
|
2360
|
+
return false;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
async function startConnectServer(config, effects, preferredPort) {
|
|
2364
|
+
const handler = (request3, response) => {
|
|
2365
|
+
if (servedByPipeline(request3.url)) {
|
|
2366
|
+
config.serveRouted(request3, response);
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
effects.forwardHttp(config.upstream, request3, response);
|
|
2370
|
+
};
|
|
2371
|
+
const httpSession = effects.createHttpSession(handler);
|
|
2372
|
+
const tlsAcceptor = effects.createTlsAcceptor(config.leafFor(config.interceptHost), (secure) => {
|
|
2373
|
+
httpSession.serve(secure);
|
|
2374
|
+
});
|
|
2375
|
+
const ledger = createConnectionLedger(config.limits, config.isLiveCapability);
|
|
2376
|
+
const listener = await effects.listenLoopback(preferredPort, (socket) => {
|
|
2377
|
+
handleConnect(socket, { config, effects, tlsAcceptor, ledger });
|
|
2378
|
+
});
|
|
2379
|
+
return {
|
|
2380
|
+
port: listener.port,
|
|
2381
|
+
close: async () => {
|
|
2382
|
+
ledger.close();
|
|
2383
|
+
await listener.close();
|
|
2384
|
+
tlsAcceptor.close();
|
|
2385
|
+
httpSession.close();
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
function createConnectionLedger(limits, isLiveCapability) {
|
|
2390
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2391
|
+
const authenticated = /* @__PURE__ */ new Map();
|
|
2392
|
+
let tunnels = 0;
|
|
2393
|
+
const leavePending = (socket) => {
|
|
2394
|
+
const deadline = pending.get(socket);
|
|
2395
|
+
if (deadline !== void 0) {
|
|
2396
|
+
clearTimeout(deadline);
|
|
2397
|
+
pending.delete(socket);
|
|
2398
|
+
}
|
|
2399
|
+
};
|
|
2400
|
+
const revalidation = setInterval(() => {
|
|
2401
|
+
for (const [capability, sockets] of authenticated) {
|
|
2402
|
+
if (!isLiveCapability(capability)) {
|
|
2403
|
+
for (const socket of sockets) {
|
|
2404
|
+
socket.destroy();
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}, limits.revalidateMs);
|
|
2409
|
+
revalidation.unref();
|
|
2410
|
+
return {
|
|
2411
|
+
accept: (socket) => {
|
|
2412
|
+
const [oldest] = pending.keys();
|
|
2413
|
+
if (pending.size >= limits.maxPendingHeads && oldest !== void 0) {
|
|
2414
|
+
leavePending(oldest);
|
|
2415
|
+
refuse(oldest, REFUSAL.requestTimeout);
|
|
2416
|
+
}
|
|
2417
|
+
pending.set(
|
|
2418
|
+
socket,
|
|
2419
|
+
setTimeout(() => {
|
|
2420
|
+
leavePending(socket);
|
|
2421
|
+
refuse(socket, REFUSAL.requestTimeout);
|
|
2422
|
+
}, limits.headDeadlineMs)
|
|
2423
|
+
);
|
|
2424
|
+
socket.once("close", () => {
|
|
2425
|
+
leavePending(socket);
|
|
2426
|
+
});
|
|
2427
|
+
},
|
|
2428
|
+
headReceived: leavePending,
|
|
2429
|
+
establish: (socket, capability) => {
|
|
2430
|
+
if (tunnels >= limits.maxTunnels) {
|
|
2431
|
+
return false;
|
|
2432
|
+
}
|
|
2433
|
+
tunnels += 1;
|
|
2434
|
+
const sockets = authenticated.get(capability) ?? /* @__PURE__ */ new Set();
|
|
2435
|
+
sockets.add(socket);
|
|
2436
|
+
authenticated.set(capability, sockets);
|
|
2437
|
+
socket.once("close", () => {
|
|
2438
|
+
tunnels -= 1;
|
|
2439
|
+
sockets.delete(socket);
|
|
2440
|
+
if (sockets.size === 0 && authenticated.get(capability) === sockets) {
|
|
2441
|
+
authenticated.delete(capability);
|
|
2442
|
+
}
|
|
2443
|
+
});
|
|
2444
|
+
return true;
|
|
2445
|
+
},
|
|
2446
|
+
close: () => {
|
|
2447
|
+
clearInterval(revalidation);
|
|
2448
|
+
for (const deadline of pending.values()) {
|
|
2449
|
+
clearTimeout(deadline);
|
|
2450
|
+
}
|
|
2451
|
+
pending.clear();
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
function refuse(socket, status, headers = []) {
|
|
2456
|
+
socket.removeAllListeners("data");
|
|
2457
|
+
socket.pause();
|
|
2458
|
+
if (socket.destroyed || socket.writableEnded) {
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
socket.end(`HTTP/1.1 ${status}\r
|
|
2462
|
+
${headers.map((header) => `${header}\r
|
|
2463
|
+
`).join("")}content-length: 0\r
|
|
2464
|
+
connection: close\r
|
|
2465
|
+
\r
|
|
2466
|
+
`, () => {
|
|
2467
|
+
socket.destroy();
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
function handleConnect(socket, context) {
|
|
2471
|
+
socket.on("error", () => {
|
|
2472
|
+
socket.destroy();
|
|
2473
|
+
});
|
|
2474
|
+
context.ledger.accept(socket);
|
|
2475
|
+
let buffer = Buffer.alloc(0);
|
|
2476
|
+
const onData = (chunk) => {
|
|
2477
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
2478
|
+
const headEnd = buffer.indexOf(HEAD_TERMINATOR);
|
|
2479
|
+
if (headEnd === -1 ? buffer.length > MAX_CONNECT_HEAD_BYTES : headEnd > MAX_CONNECT_HEAD_BYTES) {
|
|
2480
|
+
context.ledger.headReceived(socket);
|
|
2481
|
+
refuse(socket, REFUSAL.headTooLarge);
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
if (headEnd === -1) {
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
socket.removeListener("data", onData);
|
|
2488
|
+
socket.pause();
|
|
2489
|
+
context.ledger.headReceived(socket);
|
|
2490
|
+
const head = buffer.subarray(0, headEnd).toString("utf8");
|
|
2491
|
+
socket.unshift(buffer.subarray(headEnd + HEAD_TERMINATOR.length));
|
|
2492
|
+
routeConnect(socket, head, context);
|
|
2493
|
+
};
|
|
2494
|
+
socket.on("data", onData);
|
|
2495
|
+
}
|
|
2496
|
+
function proxyAuthorizationOf(head) {
|
|
2497
|
+
const values = head.split("\r\n").slice(1).flatMap((line) => {
|
|
2498
|
+
const colon = line.indexOf(":");
|
|
2499
|
+
return colon > 0 && line.slice(0, colon).trim().toLowerCase() === "proxy-authorization" ? [line.slice(colon + 1)] : [];
|
|
2500
|
+
});
|
|
2501
|
+
const [value] = values;
|
|
2502
|
+
return values.length === 1 ? value : void 0;
|
|
2503
|
+
}
|
|
2504
|
+
function routeConnect(socket, head, context) {
|
|
2505
|
+
const { config, effects, tlsAcceptor, ledger } = context;
|
|
2506
|
+
const credential = proxyAuthorizationOf(head);
|
|
2507
|
+
const capability = credential === void 0 ? void 0 : capabilityFromProxyAuthorization(credential);
|
|
2508
|
+
if (capability === void 0 || !config.isLiveCapability(capability)) {
|
|
2509
|
+
refuse(socket, REFUSAL.proxyAuthenticationRequired, [`Proxy-Authenticate: Basic realm="${CONNECT_PROXY_REALM}"`]);
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
const requestLine = head.split("\r\n", 1)[0] ?? "";
|
|
2513
|
+
const parts = requestLine.split(" ");
|
|
2514
|
+
const authority = parts.length === CONNECT_LINE_TOKENS && parts[0]?.toUpperCase() === "CONNECT" ? parts[1] ?? "" : void 0;
|
|
2515
|
+
const target = authority === void 0 ? void 0 : parseConnectTarget(authority);
|
|
2516
|
+
if (target === void 0) {
|
|
2517
|
+
refuse(socket, REFUSAL.badRequest);
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
if (!ledger.establish(socket, capability)) {
|
|
2521
|
+
refuse(socket, REFUSAL.serviceUnavailable);
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
socket.write(CONNECT_ESTABLISHED);
|
|
2525
|
+
if (isInterceptedHost(target.host, config.interceptHost)) {
|
|
2526
|
+
tlsAcceptor.accept(socket);
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
void blindTunnel(socket, target, effects);
|
|
2530
|
+
}
|
|
2531
|
+
async function blindTunnel(socket, target, effects) {
|
|
2532
|
+
let upstream;
|
|
2533
|
+
try {
|
|
2534
|
+
upstream = await effects.connectTcp(target.host, target.port);
|
|
2535
|
+
} catch {
|
|
2536
|
+
socket.destroy();
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (socket.destroyed) {
|
|
2540
|
+
upstream.destroy();
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
socket.pipe(upstream);
|
|
2544
|
+
upstream.pipe(socket);
|
|
2545
|
+
const drop = () => {
|
|
2546
|
+
socket.destroy();
|
|
2547
|
+
upstream.destroy();
|
|
2548
|
+
};
|
|
2549
|
+
socket.on("error", drop);
|
|
2550
|
+
upstream.on("error", drop);
|
|
2551
|
+
socket.on("close", () => {
|
|
2552
|
+
upstream.destroy();
|
|
2553
|
+
});
|
|
2554
|
+
upstream.on("close", () => {
|
|
2555
|
+
socket.destroy();
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
function realConnectEffects() {
|
|
2559
|
+
const plainAgent = new http.Agent({ keepAlive: true });
|
|
2560
|
+
const tlsAgent = new https.Agent({ keepAlive: true });
|
|
2561
|
+
const listenOnce = async (port, onSocket) => await new Promise((resolve, reject) => {
|
|
2562
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
2563
|
+
const server = net2.createServer((socket) => {
|
|
2564
|
+
accepted.add(socket);
|
|
2565
|
+
socket.on("close", () => {
|
|
2566
|
+
accepted.delete(socket);
|
|
2567
|
+
});
|
|
2568
|
+
onSocket(socket);
|
|
2569
|
+
});
|
|
2570
|
+
server.once("error", reject);
|
|
2571
|
+
server.listen(port, "127.0.0.1", () => {
|
|
2572
|
+
const address = server.address();
|
|
2573
|
+
const bound = typeof address === "object" && address !== null ? address.port : 0;
|
|
2574
|
+
if (bound === 0) {
|
|
2575
|
+
reject(new Error("could not bind a loopback port"));
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
resolve({
|
|
2579
|
+
port: bound,
|
|
2580
|
+
close: async () => {
|
|
2581
|
+
await new Promise((closeResolve) => {
|
|
2582
|
+
server.close(() => {
|
|
2583
|
+
closeResolve(void 0);
|
|
2584
|
+
});
|
|
2585
|
+
for (const socket of accepted) {
|
|
2586
|
+
socket.destroy();
|
|
2587
|
+
}
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
});
|
|
2591
|
+
});
|
|
2592
|
+
});
|
|
2593
|
+
return {
|
|
2594
|
+
listenLoopback: async (preferredPort, onSocket) => {
|
|
2595
|
+
if (preferredPort !== void 0) {
|
|
2596
|
+
try {
|
|
2597
|
+
return await listenOnce(preferredPort, onSocket);
|
|
2598
|
+
} catch {
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
return await listenOnce(0, onSocket);
|
|
2602
|
+
},
|
|
2603
|
+
createTlsAcceptor: (leaf, onSecure) => {
|
|
2604
|
+
const server = tls.createServer({ key: leaf.keyPem, cert: leaf.certPem }, (secure) => {
|
|
2605
|
+
onSecure(secure);
|
|
2606
|
+
});
|
|
2607
|
+
server.on("clientError", (_error, socket) => {
|
|
2608
|
+
socket.destroy();
|
|
2609
|
+
});
|
|
2610
|
+
return {
|
|
2611
|
+
// 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.
|
|
2612
|
+
accept: (socket) => {
|
|
2613
|
+
server.emit("connection", socket);
|
|
2614
|
+
},
|
|
2615
|
+
close: () => {
|
|
2616
|
+
server.close();
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
},
|
|
2620
|
+
createHttpSession: (handler) => {
|
|
2621
|
+
const server = http.createServer(handler);
|
|
2622
|
+
server.on("clientError", (_error, socket) => {
|
|
2623
|
+
socket.destroy();
|
|
2624
|
+
});
|
|
2625
|
+
return {
|
|
2626
|
+
serve: (socket) => {
|
|
2627
|
+
server.emit("connection", socket);
|
|
2628
|
+
},
|
|
2629
|
+
close: () => {
|
|
2630
|
+
server.close();
|
|
2631
|
+
server.closeAllConnections();
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
},
|
|
2635
|
+
connectTcp: async (host, port) => await new Promise((resolve, reject) => {
|
|
2636
|
+
const socket = net2.connect({ host, port });
|
|
2637
|
+
socket.once("connect", () => {
|
|
2638
|
+
resolve(socket);
|
|
2639
|
+
});
|
|
2640
|
+
socket.once("error", reject);
|
|
2641
|
+
}),
|
|
2642
|
+
forwardHttp: (target, request3, response) => {
|
|
2643
|
+
const headers = forwardableHeaders(request3.headers);
|
|
2644
|
+
const options = {
|
|
2645
|
+
host: target.host,
|
|
2646
|
+
port: target.port,
|
|
2647
|
+
method: request3.method,
|
|
2648
|
+
path: request3.url,
|
|
2649
|
+
headers,
|
|
2650
|
+
agent: target.tls ? tlsAgent : plainAgent
|
|
2651
|
+
};
|
|
2652
|
+
if (target.tls) {
|
|
2653
|
+
options.servername = target.servername ?? target.host;
|
|
2654
|
+
if (target.ca !== void 0) {
|
|
2655
|
+
options.ca = [...target.ca];
|
|
2656
|
+
}
|
|
2657
|
+
if (target.rejectUnauthorized !== void 0) {
|
|
2658
|
+
options.rejectUnauthorized = target.rejectUnauthorized;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
const onUpstreamResponse = (upstreamResponse) => {
|
|
2662
|
+
response.writeHead(upstreamResponse.statusCode ?? HTTP_BAD_GATEWAY, forwardableHeaders(upstreamResponse.headers));
|
|
2663
|
+
response.flushHeaders();
|
|
2664
|
+
upstreamResponse.pipe(response);
|
|
2665
|
+
};
|
|
2666
|
+
const upstream = target.tls ? https.request(options, onUpstreamResponse) : http.request(options, onUpstreamResponse);
|
|
2667
|
+
upstream.on("error", (error) => {
|
|
2668
|
+
if (response.headersSent) {
|
|
2669
|
+
response.destroy(error);
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
const body = `agent-shim headroom: upstream unreachable (${error.message})`;
|
|
2673
|
+
response.writeHead(HTTP_BAD_GATEWAY, { "content-type": "text/plain", "content-length": String(body.length) });
|
|
2674
|
+
response.end(body);
|
|
2675
|
+
});
|
|
2676
|
+
request3.pipe(upstream);
|
|
2677
|
+
request3.socket.setNoDelay(true);
|
|
2678
|
+
upstream.on("socket", (socket) => {
|
|
2679
|
+
socket.setNoDelay(true);
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
function isEnoent(error) {
|
|
2685
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
2686
|
+
}
|
|
2687
|
+
function realConnectCertStore(paths) {
|
|
2688
|
+
const read = (file) => {
|
|
2689
|
+
try {
|
|
2690
|
+
return fs.readFileSync(file, "utf8");
|
|
2691
|
+
} catch (error) {
|
|
2692
|
+
if (isEnoent(error)) {
|
|
2693
|
+
return void 0;
|
|
2694
|
+
}
|
|
2695
|
+
throw error;
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
return {
|
|
2699
|
+
loadCa: () => {
|
|
2700
|
+
const certPem = read(paths.frontdoorCaCertFile);
|
|
2701
|
+
const keyPem2 = read(paths.frontdoorCaKeyFile);
|
|
2702
|
+
return certPem === void 0 || keyPem2 === void 0 ? void 0 : { certPem, keyPem: keyPem2 };
|
|
2703
|
+
},
|
|
2704
|
+
writeCa: (ca) => {
|
|
2705
|
+
fs.mkdirSync(paths.frontdoorCaDir, { recursive: true });
|
|
2706
|
+
fs.writeFileSync(paths.frontdoorCaCertFile, ca.certPem, "utf8");
|
|
2707
|
+
fs.writeFileSync(paths.frontdoorCaKeyFile, ca.keyPem, { encoding: "utf8", mode: CA_KEY_FILE_MODE });
|
|
2708
|
+
fs.chmodSync(paths.frontdoorCaKeyFile, CA_KEY_FILE_MODE);
|
|
2709
|
+
}
|
|
2710
|
+
};
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
// src/frontdoor/ensure.ts
|
|
2714
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
2715
|
+
|
|
2716
|
+
// src/headroom/state.ts
|
|
2717
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2718
|
+
import path6 from "node:path";
|
|
2719
|
+
import { z as z4 } from "zod";
|
|
2720
|
+
var HEADROOM_ANTHROPIC_UPSTREAM = "https://api.anthropic.com";
|
|
2721
|
+
var HeadroomStateSchema = z4.strictObject({
|
|
2722
|
+
/** The supervisor process owning the daemon. Alive means someone is keeping headroom running. */
|
|
2723
|
+
supervisorPid: z4.number().int().positive().optional(),
|
|
2724
|
+
/** The headroom proxy process itself. Absent while the supervisor is between restarts. */
|
|
2725
|
+
headroomPid: z4.number().int().positive().optional(),
|
|
2726
|
+
/** 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. */
|
|
2727
|
+
port: z4.number().int().positive().optional(),
|
|
2728
|
+
/**
|
|
2729
|
+
* 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.
|
|
2730
|
+
*/
|
|
2731
|
+
lastPort: z4.number().int().positive().optional(),
|
|
2732
|
+
/** The `headroom --version` output of the running install. */
|
|
2733
|
+
version: z4.string().optional(),
|
|
2734
|
+
/** Hash of the allowlist the running proxy was started with, so a provider-file change is detected as drift. */
|
|
2735
|
+
allowlistHash: z4.string().optional(),
|
|
2736
|
+
/** Hash of the token-saving settings the running proxy was started with, so a settings change is detected as drift. */
|
|
2737
|
+
settingsHash: z4.string().optional(),
|
|
2738
|
+
/** The install spec the running install was last installed from, so a `source` config change is detected as drift. */
|
|
2739
|
+
installedSource: z4.string().optional(),
|
|
2740
|
+
/** The last fatal error, kept across shutdowns so `headroom status` can explain a daemon that is not running. */
|
|
2741
|
+
lastError: z4.string().optional()
|
|
2742
|
+
});
|
|
2743
|
+
var HeadroomSessionSchema = z4.strictObject({ pid: z4.number().int().positive(), startedAt: z4.number(), supervisorPid: z4.number().int().positive() });
|
|
2744
|
+
var HeadroomLockSchema = z4.strictObject({ pid: z4.number().int().positive(), at: z4.number() });
|
|
2745
|
+
function parseJson(raw, schema) {
|
|
2746
|
+
if (raw === void 0 || raw.trim() === "") {
|
|
2747
|
+
return void 0;
|
|
2748
|
+
}
|
|
2749
|
+
const parsed = schema.safeParse(JSON.parse(raw));
|
|
2750
|
+
return parsed.success ? parsed.data : void 0;
|
|
2751
|
+
}
|
|
2752
|
+
function readHeadroomState(fs2, stateFile) {
|
|
2753
|
+
try {
|
|
2754
|
+
return parseJson(fs2.readFileUtf8(stateFile), HeadroomStateSchema);
|
|
2755
|
+
} catch {
|
|
2756
|
+
return void 0;
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
function writeHeadroomState(fs2, stateFile, state) {
|
|
2760
|
+
fs2.mkdirp(path6.dirname(stateFile));
|
|
2761
|
+
fs2.writeFilePrivate(stateFile, `${JSON.stringify(state, null, 2)}
|
|
2762
|
+
`);
|
|
2763
|
+
}
|
|
2764
|
+
function headroomAllowlist(providers) {
|
|
2765
|
+
const urls = /* @__PURE__ */ new Set([HEADROOM_ANTHROPIC_UPSTREAM]);
|
|
2766
|
+
for (const provider of providers) {
|
|
2767
|
+
urls.add(provider.baseUrl);
|
|
2768
|
+
}
|
|
2769
|
+
return [...urls].sort();
|
|
2770
|
+
}
|
|
2771
|
+
function headroomUpstreams(providers, frontDoorOrigin2) {
|
|
2772
|
+
const upstreams = providers.filter((provider) => !isCodexProvider(provider)).map((provider) => ({ baseUrl: provider.baseUrl }));
|
|
2773
|
+
if (providers.length > 0 && frontDoorOrigin2 !== void 0) {
|
|
2774
|
+
upstreams.push({ baseUrl: frontDoorOrigin2 });
|
|
2775
|
+
}
|
|
2776
|
+
return upstreams;
|
|
2777
|
+
}
|
|
2778
|
+
function hashAllowlist(allowlist) {
|
|
2779
|
+
return createHash2("sha256").update(allowlist.join("\n")).digest("hex");
|
|
2780
|
+
}
|
|
2781
|
+
function readAllProviders(fs2, providersDir) {
|
|
2782
|
+
const result = [];
|
|
2783
|
+
for (const name of fs2.readdir(providersDir)) {
|
|
2784
|
+
if (!name.endsWith(".json")) {
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
try {
|
|
2788
|
+
const parsed = parseJson(fs2.readFileUtf8(path6.join(providersDir, name)), ProviderSchema);
|
|
2789
|
+
if (parsed !== void 0) {
|
|
2790
|
+
result.push({ name: name.slice(0, -".json".length), provider: parsed });
|
|
2791
|
+
}
|
|
2792
|
+
} catch {
|
|
2793
|
+
continue;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return result;
|
|
2797
|
+
}
|
|
2798
|
+
function writeSession(fs2, sessionsDir, session) {
|
|
2799
|
+
fs2.mkdirp(sessionsDir);
|
|
2800
|
+
fs2.writeFileUtf8(path6.join(sessionsDir, `${String(session.pid)}.json`), `${JSON.stringify(session, null, 2)}
|
|
2801
|
+
`);
|
|
2802
|
+
}
|
|
2803
|
+
function removeSession(fs2, sessionsDir, pid) {
|
|
2804
|
+
fs2.removeRecursive(path6.join(sessionsDir, `${String(pid)}.json`));
|
|
2805
|
+
}
|
|
2806
|
+
function listSessions(fs2, sessionsDir) {
|
|
2807
|
+
const sessions = [];
|
|
2808
|
+
for (const name of fs2.readdir(sessionsDir)) {
|
|
2809
|
+
if (!name.endsWith(".json")) {
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
try {
|
|
2813
|
+
const parsed = parseJson(fs2.readFileUtf8(path6.join(sessionsDir, name)), HeadroomSessionSchema);
|
|
2814
|
+
if (parsed !== void 0) {
|
|
2815
|
+
sessions.push(parsed);
|
|
2816
|
+
}
|
|
2817
|
+
} catch {
|
|
2818
|
+
continue;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
return sessions.sort((a, b) => a.pid - b.pid);
|
|
2822
|
+
}
|
|
2823
|
+
function sessionsForSupervisor(fs2, sessionsDir, supervisorPid) {
|
|
2824
|
+
return listSessions(fs2, sessionsDir).filter((session) => session.supervisorPid === supervisorPid);
|
|
2825
|
+
}
|
|
2826
|
+
function pruneDeadSessions(fs2, sessionsDir, isRunning) {
|
|
2827
|
+
const removed = [];
|
|
2828
|
+
for (const session of listSessions(fs2, sessionsDir)) {
|
|
2829
|
+
if (!isRunning(session.pid)) {
|
|
2830
|
+
removeSession(fs2, sessionsDir, session.pid);
|
|
2831
|
+
removed.push(session.pid);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
return removed;
|
|
2835
|
+
}
|
|
2836
|
+
function readStartLock(fs2, lockFile) {
|
|
2837
|
+
try {
|
|
2838
|
+
return parseJson(fs2.readFileUtf8(lockFile), HeadroomLockSchema);
|
|
2839
|
+
} catch {
|
|
2840
|
+
return void 0;
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// src/frontdoor/ensure.ts
|
|
2845
|
+
var FRONTDOOR_START_TIMEOUT_MS = 3e4;
|
|
2846
|
+
var FRONTDOOR_LAUNCHER_POLL_MS = 100;
|
|
2847
|
+
var FrontDoorStartError = class extends CliError {
|
|
2848
|
+
constructor(message) {
|
|
2849
|
+
super(message);
|
|
2850
|
+
this.name = "FrontDoorStartError";
|
|
2851
|
+
}
|
|
2852
|
+
};
|
|
2853
|
+
function ensureFrontDoor(params) {
|
|
2854
|
+
const { paths, ports } = params;
|
|
2855
|
+
const deadline = ports.now() + FRONTDOOR_START_TIMEOUT_MS;
|
|
2856
|
+
let spawned = false;
|
|
2857
|
+
const distrusted = /* @__PURE__ */ new Set();
|
|
2858
|
+
let lastRejection;
|
|
2859
|
+
for (; ; ) {
|
|
2860
|
+
const state = readFrontDoorState(ports.fs, paths.frontdoorStateFile);
|
|
2861
|
+
const supervisorPid = state?.supervisorPid;
|
|
2862
|
+
if (supervisorPid !== void 0 && !distrusted.has(supervisorPid) && ports.isRunning(supervisorPid)) {
|
|
2863
|
+
if (state?.port !== void 0 && state.connectPort !== void 0) {
|
|
2864
|
+
const verdict = ports.verifyListener(state.port);
|
|
2865
|
+
if (verdict.ok) {
|
|
2866
|
+
const token = randomUUID2();
|
|
2867
|
+
writeFrontDoorSession(ports.fs, paths.frontdoorSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), token });
|
|
2868
|
+
return { port: state.port, connectPort: state.connectPort, token };
|
|
2869
|
+
}
|
|
2870
|
+
lastRejection = `the listener on 127.0.0.1:${String(state.port)} (recorded for pid ${String(supervisorPid)}) failed authentication: ${verdict.reason}`;
|
|
2871
|
+
if (spawned) {
|
|
2872
|
+
throw new FrontDoorStartError(`agent-shim: ${lastRejection}; refusing to send this session's credentials to it. Daemon log: ${paths.frontdoorLogPath}`);
|
|
2873
|
+
}
|
|
2874
|
+
distrusted.add(supervisorPid);
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
if (state?.lastError !== void 0) {
|
|
2878
|
+
throw new FrontDoorStartError(`agent-shim: the front door reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.frontdoorLogPath})`);
|
|
2879
|
+
}
|
|
2880
|
+
} else if (!spawned) {
|
|
2881
|
+
ports.fs.mkdirp(paths.frontdoorDir);
|
|
2882
|
+
const created = ports.fs.writeFileExclusive(paths.frontdoorLockFile, `${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
|
|
2883
|
+
`);
|
|
2884
|
+
if (created) {
|
|
2885
|
+
ports.spawnSupervisor(paths);
|
|
2886
|
+
spawned = true;
|
|
2887
|
+
} else {
|
|
2888
|
+
const lock = readStartLock(ports.fs, paths.frontdoorLockFile);
|
|
2889
|
+
if (lock === void 0 || !ports.isRunning(lock.pid)) {
|
|
2890
|
+
ports.fs.removeRecursive(paths.frontdoorLockFile);
|
|
2891
|
+
continue;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
if (ports.now() >= deadline) {
|
|
2896
|
+
const lastError = state?.lastError;
|
|
2897
|
+
throw new FrontDoorStartError(
|
|
2898
|
+
`agent-shim: 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}`
|
|
2899
|
+
);
|
|
2900
|
+
}
|
|
2901
|
+
ports.sleep(FRONTDOOR_LAUNCHER_POLL_MS);
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
// src/usage/read.ts
|
|
2906
|
+
import path7 from "node:path";
|
|
2907
|
+
|
|
2908
|
+
// src/usage/schema.ts
|
|
2909
|
+
import { z as z5 } from "zod";
|
|
2910
|
+
var USAGE_SCHEMA_VERSION = 1;
|
|
2911
|
+
var VersionSchema = z5.literal(USAGE_SCHEMA_VERSION);
|
|
2912
|
+
var InstantSchema = z5.iso.datetime();
|
|
2913
|
+
var TokenCountSchema = z5.number().int().nonnegative();
|
|
2914
|
+
var TokenUsageSchema = z5.strictObject({
|
|
2915
|
+
inputTokens: TokenCountSchema.optional(),
|
|
2916
|
+
outputTokens: TokenCountSchema.optional(),
|
|
2917
|
+
cacheCreationInputTokens: TokenCountSchema.optional(),
|
|
2918
|
+
cacheReadInputTokens: TokenCountSchema.optional()
|
|
2919
|
+
});
|
|
2920
|
+
var LIMIT_KINDS = ["rate-limited", "quota-exhausted"];
|
|
2921
|
+
var LimitClassificationSchema = z5.strictObject({
|
|
2922
|
+
kind: z5.enum(LIMIT_KINDS),
|
|
2923
|
+
/** Seconds to wait before retrying, from the response's `retry-after`. */
|
|
2924
|
+
retryAfterSeconds: z5.number().nonnegative().optional(),
|
|
2925
|
+
/** When the limiting window resets, from the upstream's own reset header. */
|
|
2926
|
+
resetAt: InstantSchema.optional(),
|
|
2927
|
+
/** The window the upstream named as the binding one (`anthropic-ratelimit-unified-representative-claim`), such as `five_hour` or `seven_day`. */
|
|
2928
|
+
window: z5.string().optional(),
|
|
2929
|
+
/** The signals the classification rests on, each `<name>=<value>` from a response header or the error body's `type` or `code`. Never the error message. */
|
|
2930
|
+
evidence: z5.array(z5.string())
|
|
2931
|
+
});
|
|
2932
|
+
var UsageRecordSchema = z5.strictObject({
|
|
2933
|
+
schemaVersion: VersionSchema,
|
|
2934
|
+
/** When the front door received the request. */
|
|
2935
|
+
at: InstantSchema,
|
|
2936
|
+
/** The launching identity, when the launch resolved one. */
|
|
2937
|
+
identity: z5.string().optional(),
|
|
2938
|
+
/** `anthropic` for an OAuth session's request to Claude Code's own API, otherwise the provider's name. */
|
|
2939
|
+
provider: z5.string(),
|
|
2940
|
+
/** The launch's session id, generated per launch. */
|
|
2941
|
+
sessionId: z5.string().optional(),
|
|
2942
|
+
/** The project the launch ran in (its git repository root), when the launch sent one: headroom launches do. */
|
|
2943
|
+
project: z5.string().optional(),
|
|
2944
|
+
/** The front-door route that served the request. */
|
|
2945
|
+
route: z5.string(),
|
|
2946
|
+
method: z5.string(),
|
|
2947
|
+
/** The API path as the upstream sees it (`/v1/messages`), without the provider prefix or a query string. */
|
|
2948
|
+
endpoint: z5.string(),
|
|
2949
|
+
/** The model the response named. */
|
|
2950
|
+
model: z5.string().optional(),
|
|
2951
|
+
status: z5.number().int(),
|
|
2952
|
+
/** Milliseconds from the request's arrival to its response head. */
|
|
2953
|
+
latencyMs: z5.number().int().nonnegative(),
|
|
2954
|
+
/** Milliseconds from the request's arrival to the end of its response body. */
|
|
2955
|
+
durationMs: z5.number().int().nonnegative(),
|
|
2956
|
+
/** Whether the client received the whole body. */
|
|
2957
|
+
outcome: z5.enum(["completed", "aborted"]),
|
|
2958
|
+
usage: TokenUsageSchema.optional(),
|
|
2959
|
+
/** The response's quota and rate-limit headers, by name (see `isRateLimitHeader`). */
|
|
2960
|
+
rateLimitHeaders: z5.record(z5.string(), z5.string()).optional(),
|
|
2961
|
+
limit: LimitClassificationSchema.optional(),
|
|
2962
|
+
/** The upstream's request id (`request-id`), for matching a record to the provider's own logs. */
|
|
2963
|
+
requestId: z5.string().optional()
|
|
2964
|
+
});
|
|
2965
|
+
var QuotaWindowSchema = z5.strictObject({
|
|
2966
|
+
/** The fraction of the window used, as the upstream reports it (0 to 1). */
|
|
2967
|
+
utilization: z5.number().nonnegative().optional(),
|
|
2968
|
+
resetsAt: InstantSchema.optional(),
|
|
2969
|
+
/** The window's own status (`allowed`, `allowed_warning`, `rejected`). */
|
|
2970
|
+
status: z5.string().optional()
|
|
2971
|
+
});
|
|
2972
|
+
var UnifiedRateLimitSchema = z5.strictObject({
|
|
2973
|
+
/** The overall status: `allowed`, `allowed_warning` or `rejected`. */
|
|
2974
|
+
status: z5.string().optional(),
|
|
2975
|
+
fiveHour: QuotaWindowSchema.optional(),
|
|
2976
|
+
sevenDay: QuotaWindowSchema.optional(),
|
|
2977
|
+
/** The window that currently binds. */
|
|
2978
|
+
representativeClaim: z5.string().optional(),
|
|
2979
|
+
/** When the binding window resets. */
|
|
2980
|
+
resetAt: InstantSchema.optional(),
|
|
2981
|
+
/** Whether usage beyond the plan (extra usage) is available: `allowed` or `rejected`. */
|
|
2982
|
+
overageStatus: z5.string().optional()
|
|
2983
|
+
});
|
|
2984
|
+
var RateLimitStateSchema = z5.strictObject({
|
|
2985
|
+
observedAt: InstantSchema,
|
|
2986
|
+
/** Every quota and rate-limit header on that response, by name. */
|
|
2987
|
+
headers: z5.record(z5.string(), z5.string()),
|
|
2988
|
+
unified: UnifiedRateLimitSchema.optional()
|
|
2989
|
+
});
|
|
2990
|
+
var LimitEventSchema = LimitClassificationSchema.extend({
|
|
2991
|
+
observedAt: InstantSchema,
|
|
2992
|
+
status: z5.number().int()
|
|
2993
|
+
});
|
|
2994
|
+
var ProviderQuotaWindowSchema = z5.strictObject({
|
|
2995
|
+
/** What the window measures, in the provider's terms (`tokens`, `tool-calls`). */
|
|
2996
|
+
measures: z5.string(),
|
|
2997
|
+
/** The window's length in milliseconds, when the provider's unit is one this project knows; absent for an unrecognised unit, with `period` carrying the raw code. */
|
|
2998
|
+
periodMs: z5.number().int().positive().optional(),
|
|
2999
|
+
/** The provider's own description of the period when `periodMs` could not be derived, such as `unit 4 x 1`. */
|
|
3000
|
+
period: z5.string().optional(),
|
|
3001
|
+
/** The fraction of the window used (0 to 1). */
|
|
3002
|
+
utilization: z5.number().nonnegative(),
|
|
3003
|
+
resetsAt: InstantSchema.optional(),
|
|
3004
|
+
/** Absolute counts, when the provider reports them for this window. */
|
|
3005
|
+
limit: z5.number().nonnegative().optional(),
|
|
3006
|
+
used: z5.number().nonnegative().optional(),
|
|
3007
|
+
remaining: z5.number().nonnegative().optional()
|
|
3008
|
+
});
|
|
3009
|
+
var ProviderQuotaSchema = z5.strictObject({
|
|
3010
|
+
observedAt: InstantSchema,
|
|
3011
|
+
/** Which usage endpoint produced it (`z.ai`). */
|
|
3012
|
+
source: z5.string(),
|
|
3013
|
+
/** The subscription level the endpoint reports, when it does. */
|
|
3014
|
+
level: z5.string().optional(),
|
|
3015
|
+
windows: z5.array(ProviderQuotaWindowSchema)
|
|
3016
|
+
});
|
|
3017
|
+
var ProviderUsageStateSchema = z5.strictObject({
|
|
3018
|
+
lastRequestAt: InstantSchema,
|
|
3019
|
+
lastStatus: z5.number().int(),
|
|
3020
|
+
lastModel: z5.string().optional(),
|
|
3021
|
+
/** The latest rate-limit state, from the last response that carried any rate-limit header. */
|
|
3022
|
+
rateLimit: RateLimitStateSchema.optional(),
|
|
3023
|
+
/** The latest classified refusal. It stays after later successes, so a reader compares its `resetAt` (or `observedAt` plus `retryAfterSeconds`) with the current time. */
|
|
3024
|
+
lastLimit: LimitEventSchema.optional(),
|
|
3025
|
+
/** The latest quota the provider's usage endpoint reported, for providers whose quota is not visible in response headers. */
|
|
3026
|
+
quota: ProviderQuotaSchema.optional()
|
|
3027
|
+
});
|
|
3028
|
+
var AccountMetadataSchema = z5.strictObject({
|
|
3029
|
+
accountUuid: z5.string().optional(),
|
|
3030
|
+
emailAddress: z5.string().optional(),
|
|
3031
|
+
displayName: z5.string().optional(),
|
|
3032
|
+
organizationUuid: z5.string().optional(),
|
|
3033
|
+
organizationName: z5.string().optional(),
|
|
3034
|
+
organizationType: z5.string().optional(),
|
|
3035
|
+
organizationRole: z5.string().optional(),
|
|
3036
|
+
workspaceRole: z5.string().optional(),
|
|
3037
|
+
billingType: z5.string().optional(),
|
|
3038
|
+
seatTier: z5.string().optional(),
|
|
3039
|
+
organizationRateLimitTier: z5.string().optional(),
|
|
3040
|
+
userRateLimitTier: z5.string().optional(),
|
|
3041
|
+
hasExtraUsageEnabled: z5.boolean().optional(),
|
|
3042
|
+
subscriptionCreatedAt: z5.string().optional(),
|
|
3043
|
+
accountCreatedAt: z5.string().optional()
|
|
3044
|
+
});
|
|
3045
|
+
var UsageSnapshotSchema = z5.strictObject({
|
|
3046
|
+
schemaVersion: VersionSchema,
|
|
3047
|
+
identity: z5.string(),
|
|
3048
|
+
updatedAt: InstantSchema,
|
|
3049
|
+
/** The identity's account metadata as of `updatedAt`. */
|
|
3050
|
+
account: AccountMetadataSchema.optional(),
|
|
3051
|
+
/** Latest state per provider, keyed by provider name (`anthropic` for OAuth sessions). */
|
|
3052
|
+
providers: z5.record(z5.string(), ProviderUsageStateSchema)
|
|
3053
|
+
});
|
|
3054
|
+
|
|
3055
|
+
// src/usage/account.ts
|
|
3056
|
+
function isIdentityName(name) {
|
|
3057
|
+
return IdentitySchema.shape.name.safeParse(name).success;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
// src/usage/read.ts
|
|
3061
|
+
var UsageSnapshotError = class extends Error {
|
|
3062
|
+
constructor(file, detail) {
|
|
3063
|
+
super(`${file} is not a usage snapshot this agent-shim can read (schema version ${String(USAGE_SCHEMA_VERSION)}): ${detail}`);
|
|
3064
|
+
this.file = file;
|
|
3065
|
+
this.name = "UsageSnapshotError";
|
|
3066
|
+
}
|
|
3067
|
+
file;
|
|
3068
|
+
};
|
|
3069
|
+
function snapshotPath(snapshotsDir, identity) {
|
|
3070
|
+
if (!isIdentityName(identity)) {
|
|
3071
|
+
throw new Error(`"${identity}" is not a valid identity name.`);
|
|
3072
|
+
}
|
|
3073
|
+
return path7.join(snapshotsDir, `${identity}.json`);
|
|
3074
|
+
}
|
|
3075
|
+
function readUsageSnapshot(fs2, snapshotsDir, identity) {
|
|
3076
|
+
const file = snapshotPath(snapshotsDir, identity);
|
|
3077
|
+
const raw = fs2.readFileUtf8(file);
|
|
3078
|
+
if (raw === void 0) {
|
|
3079
|
+
return void 0;
|
|
3080
|
+
}
|
|
3081
|
+
let parsed;
|
|
3082
|
+
try {
|
|
3083
|
+
parsed = JSON.parse(raw);
|
|
3084
|
+
} catch {
|
|
3085
|
+
throw new UsageSnapshotError(file, "not valid JSON");
|
|
3086
|
+
}
|
|
3087
|
+
const snapshot = UsageSnapshotSchema.safeParse(parsed);
|
|
3088
|
+
if (!snapshot.success) {
|
|
3089
|
+
throw new UsageSnapshotError(file, snapshot.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; "));
|
|
3090
|
+
}
|
|
3091
|
+
return snapshot.data;
|
|
3092
|
+
}
|
|
3093
|
+
function listUsageSnapshots(fs2, snapshotsDir) {
|
|
3094
|
+
return fs2.readdir(snapshotsDir).flatMap((name) => {
|
|
3095
|
+
if (!name.endsWith(".json")) {
|
|
3096
|
+
return [];
|
|
3097
|
+
}
|
|
3098
|
+
const identity = name.slice(0, -".json".length);
|
|
3099
|
+
if (!isIdentityName(identity)) {
|
|
3100
|
+
return [];
|
|
3101
|
+
}
|
|
3102
|
+
const snapshot = readUsageSnapshot(fs2, snapshotsDir, identity);
|
|
3103
|
+
return snapshot === void 0 ? [] : [snapshot];
|
|
3104
|
+
}).sort((left, right) => left.identity.localeCompare(right.identity));
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
// src/headroom/ensure.ts
|
|
3108
|
+
var HEADROOM_START_TIMEOUT_MS = 12e4;
|
|
3109
|
+
var HEADROOM_LAUNCHER_POLL_MS = 250;
|
|
3110
|
+
var HeadroomStartError = class extends CliError {
|
|
3111
|
+
constructor(message) {
|
|
3112
|
+
super(message);
|
|
3113
|
+
this.name = "HeadroomStartError";
|
|
3114
|
+
}
|
|
3115
|
+
};
|
|
3116
|
+
function ensureHeadroom(params) {
|
|
3117
|
+
const { paths, ports } = params;
|
|
3118
|
+
const deadline = ports.now() + HEADROOM_START_TIMEOUT_MS;
|
|
3119
|
+
let spawnedSupervisor = false;
|
|
3120
|
+
for (; ; ) {
|
|
3121
|
+
const state = readHeadroomState(ports.fs, paths.headroomStateFile);
|
|
3122
|
+
if (state?.supervisorPid !== void 0 && ports.isRunning(state.supervisorPid)) {
|
|
3123
|
+
const daemonUp = state.port !== void 0 && state.headroomPid !== void 0 && ports.isRunning(state.headroomPid);
|
|
3124
|
+
if (daemonUp && state.port !== void 0) {
|
|
3125
|
+
writeSession(ports.fs, paths.headroomSessionsDir, { pid: params.launcherPid, startedAt: ports.now(), supervisorPid: state.supervisorPid });
|
|
3126
|
+
return { port: state.port };
|
|
3127
|
+
}
|
|
3128
|
+
if (state.lastError !== void 0) {
|
|
3129
|
+
throw new HeadroomStartError(
|
|
3130
|
+
`agent-shim: the headroom daemon reported a fatal error and is not serving: ${state.lastError} (daemon log: ${paths.headroomLogPath})`
|
|
3131
|
+
);
|
|
3132
|
+
}
|
|
3133
|
+
} else if (!spawnedSupervisor) {
|
|
3134
|
+
ports.fs.mkdirp(paths.headroomDir);
|
|
3135
|
+
const created = ports.fs.writeFileExclusive(
|
|
3136
|
+
paths.headroomLockFile,
|
|
3137
|
+
`${JSON.stringify({ pid: params.launcherPid, at: ports.now() })}
|
|
3138
|
+
`
|
|
3139
|
+
);
|
|
3140
|
+
if (created) {
|
|
3141
|
+
ports.spawnSupervisor(paths);
|
|
3142
|
+
spawnedSupervisor = true;
|
|
3143
|
+
} else {
|
|
3144
|
+
const lock = readStartLock(ports.fs, paths.headroomLockFile);
|
|
3145
|
+
if (lock === void 0 || !ports.isRunning(lock.pid)) {
|
|
3146
|
+
ports.fs.removeRecursive(paths.headroomLockFile);
|
|
3147
|
+
continue;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
if (ports.now() >= deadline) {
|
|
3152
|
+
const lastError = state?.lastError;
|
|
3153
|
+
throw new HeadroomStartError(
|
|
3154
|
+
`agent-shim: the headroom daemon did not become ready within ${String(HEADROOM_START_TIMEOUT_MS)}ms` + (lastError === void 0 ? "" : ` (last error: ${lastError})`) + `. Daemon log: ${paths.headroomLogPath}`
|
|
3155
|
+
);
|
|
3156
|
+
}
|
|
3157
|
+
ports.sleep(HEADROOM_LAUNCHER_POLL_MS);
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
// src/headroom/settings.ts
|
|
3162
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
3163
|
+
function settingsOf(config) {
|
|
3164
|
+
return {
|
|
3165
|
+
...config.mode === void 0 ? {} : { mode: config.mode },
|
|
3166
|
+
...config.targetRatio === void 0 ? {} : { targetRatio: config.targetRatio },
|
|
3167
|
+
...config.ccr === void 0 ? {} : { ccr: config.ccr },
|
|
3168
|
+
...config.rolloutChannel === void 0 ? {} : { rolloutChannel: config.rolloutChannel },
|
|
3169
|
+
...config.interceptToolResults === void 0 ? {} : { interceptToolResults: config.interceptToolResults },
|
|
3170
|
+
...config.readMaturation === void 0 ? {} : { readMaturation: config.readMaturation }
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
function settingsArgs(settings) {
|
|
3174
|
+
return [
|
|
3175
|
+
...settings.mode === void 0 ? [] : ["--mode", settings.mode],
|
|
3176
|
+
...settings.targetRatio === void 0 ? [] : ["--target-ratio", String(settings.targetRatio)],
|
|
3177
|
+
...settings.ccr === "lossless" ? ["--lossless"] : [],
|
|
3178
|
+
...settings.ccr === "none" ? ["--no-ccr"] : [],
|
|
3179
|
+
...settings.interceptToolResults === true ? ["--intercept-tool-results"] : [],
|
|
3180
|
+
...settings.readMaturation === true ? ["--read-maturation"] : []
|
|
3181
|
+
];
|
|
3182
|
+
}
|
|
3183
|
+
function settingsEnv(settings) {
|
|
3184
|
+
return settings.rolloutChannel === void 0 ? {} : { HEADROOM_ROLLOUT_CHANNEL: settings.rolloutChannel };
|
|
3185
|
+
}
|
|
3186
|
+
function hashSettings(settings) {
|
|
3187
|
+
return createHash3("sha256").update(JSON.stringify([...settingsArgs(settings), JSON.stringify(settingsEnv(settings))])).digest("hex");
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
// src/headroom/source.ts
|
|
3191
|
+
import { z as z6 } from "zod";
|
|
3192
|
+
var GIT_SPEC_RE = /git\+[^@\s]+(?:@([^\s#]+))?/;
|
|
3193
|
+
var COMMIT_SHA_RE = /^[0-9a-f]{40}$/i;
|
|
3194
|
+
var DirectUrlSchema = z6.object({ vcs_info: z6.object({ commit_id: z6.string() }) });
|
|
3195
|
+
function pinnedGitCommit(spec) {
|
|
3196
|
+
const ref = GIT_SPEC_RE.exec(spec)?.[1];
|
|
3197
|
+
return ref !== void 0 && COMMIT_SHA_RE.test(ref) ? ref.toLowerCase() : void 0;
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// src/headroom/supervisor.ts
|
|
3201
|
+
function resolveSupervisorConfig(configured) {
|
|
3202
|
+
return {
|
|
3203
|
+
source: configured.source ?? HEADROOM_DEFAULT_SOURCE,
|
|
3204
|
+
idleShutdownMinutes: configured.idleShutdownMinutes ?? HEADROOM_DEFAULT_IDLE_SHUTDOWN_MINUTES,
|
|
3205
|
+
settings: settingsOf(configured)
|
|
3206
|
+
};
|
|
3207
|
+
}
|
|
3208
|
+
var HEADROOM_BACKOFF_BASE_MS = 500;
|
|
3209
|
+
var HEADROOM_BACKOFF_CAP_MS = 3e4;
|
|
3210
|
+
var HEADROOM_START_RETRY_BUDGET = 5;
|
|
3211
|
+
var HEADROOM_READY_TIMEOUT_MS = 15e3;
|
|
3212
|
+
var HEADROOM_POLL_MS = 1e3;
|
|
3213
|
+
var MS_PER_MINUTE2 = 6e4;
|
|
3214
|
+
function backoffForAttempt(attempt) {
|
|
3215
|
+
return Math.min(HEADROOM_BACKOFF_CAP_MS, HEADROOM_BACKOFF_BASE_MS * 2 ** (attempt - 1));
|
|
3216
|
+
}
|
|
3217
|
+
function versionSatisfies(version, source) {
|
|
3218
|
+
if (version === void 0) {
|
|
3219
|
+
return false;
|
|
3220
|
+
}
|
|
3221
|
+
const specifier = /==|>=|<=|!=|>|<|~=/.exec(source);
|
|
3222
|
+
if (specifier === null) {
|
|
3223
|
+
return true;
|
|
3224
|
+
}
|
|
3225
|
+
const installed = parseVersion(version);
|
|
3226
|
+
const wanted = parseVersion(source.slice(specifier.index + specifier[0].length));
|
|
3227
|
+
if (installed === void 0 || wanted === void 0) {
|
|
3228
|
+
return true;
|
|
3229
|
+
}
|
|
3230
|
+
const cmp = compareVersions(installed, wanted);
|
|
3231
|
+
switch (specifier[0]) {
|
|
3232
|
+
case "==":
|
|
3233
|
+
return cmp === 0;
|
|
3234
|
+
case "!=":
|
|
3235
|
+
return cmp !== 0;
|
|
3236
|
+
case ">=":
|
|
3237
|
+
return cmp >= 0;
|
|
3238
|
+
case "<=":
|
|
3239
|
+
return cmp <= 0;
|
|
3240
|
+
case ">":
|
|
3241
|
+
return cmp > 0;
|
|
3242
|
+
case "<":
|
|
3243
|
+
return cmp < 0;
|
|
3244
|
+
case "~=":
|
|
3245
|
+
return compareVersions(installed.slice(0, Math.max(0, wanted.length - 1)), wanted.slice(0, Math.max(0, wanted.length - 1))) === 0 && cmp >= 0;
|
|
3246
|
+
default:
|
|
3247
|
+
return true;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
function parseVersion(input) {
|
|
3251
|
+
const match = /(\d+)(\.\d+)*/.exec(input);
|
|
3252
|
+
if (match === null) {
|
|
3253
|
+
return void 0;
|
|
3254
|
+
}
|
|
3255
|
+
return match[0].split(".").map((part) => Number.parseInt(part, 10));
|
|
3256
|
+
}
|
|
3257
|
+
function compareVersions(a, b) {
|
|
3258
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
3259
|
+
const left = a[index] ?? 0;
|
|
3260
|
+
const right = b[index] ?? 0;
|
|
3261
|
+
if (left !== right) {
|
|
3262
|
+
return left - right;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
return 0;
|
|
3266
|
+
}
|
|
3267
|
+
var HEADROOM_SUPERVISOR_STILL_RUNNING = -1;
|
|
3268
|
+
async function runSupervisor(initialConfig, ports, options = {}) {
|
|
3269
|
+
const { fs: fs2, paths } = ports;
|
|
3270
|
+
let config = initialConfig;
|
|
3271
|
+
const previousState = readHeadroomState(fs2, paths.headroomStateFile);
|
|
3272
|
+
if (previousState?.supervisorPid !== void 0 && previousState.supervisorPid !== ports.ownPid && ports.isRunning(previousState.supervisorPid)) {
|
|
3273
|
+
ports.log(`agent-shim headroom supervisor ${String(ports.ownPid)}: supervisor ${String(previousState.supervisorPid)} already owns the daemon; exiting`);
|
|
3274
|
+
return 0;
|
|
3275
|
+
}
|
|
3276
|
+
let installedSource = previousState?.installedSource;
|
|
3277
|
+
let lastPort = previousState?.lastPort;
|
|
3278
|
+
const sticky = () => lastPort === void 0 ? {} : { lastPort };
|
|
3279
|
+
const ownsState = () => {
|
|
3280
|
+
const owner = readHeadroomState(fs2, paths.headroomStateFile)?.supervisorPid;
|
|
3281
|
+
return owner === void 0 || owner === ports.ownPid || !ports.isRunning(owner);
|
|
3282
|
+
};
|
|
3283
|
+
const writeOwnState = (state) => {
|
|
3284
|
+
if (!ownsState()) {
|
|
3285
|
+
return false;
|
|
3286
|
+
}
|
|
3287
|
+
writeHeadroomState(fs2, paths.headroomStateFile, state);
|
|
3288
|
+
return true;
|
|
3289
|
+
};
|
|
3290
|
+
const fail = (message) => {
|
|
3291
|
+
if (writeOwnState({ lastError: message, ...sticky() })) {
|
|
3292
|
+
fs2.removeRecursive(paths.headroomLockFile);
|
|
3293
|
+
}
|
|
3294
|
+
ports.log(`agent-shim headroom supervisor: ${message}`);
|
|
3295
|
+
return 1;
|
|
3296
|
+
};
|
|
3297
|
+
const ensureInstalled = () => {
|
|
3298
|
+
const version2 = ports.headroomVersion();
|
|
3299
|
+
const versionOk = version2 !== void 0 && versionSatisfies(version2, config.source);
|
|
3300
|
+
const pinned = pinnedGitCommit(config.source);
|
|
3301
|
+
const commitOk = pinned === void 0 || ports.installedCommit() === pinned;
|
|
3302
|
+
if (versionOk && commitOk && (installedSource === void 0 || installedSource === config.source)) {
|
|
3303
|
+
installedSource = config.source;
|
|
3304
|
+
return true;
|
|
3305
|
+
}
|
|
3306
|
+
ports.log(`agent-shim headroom supervisor: installing headroom from ${config.source}`);
|
|
3307
|
+
const result = ports.install(config.source);
|
|
3308
|
+
if (!result.ok) {
|
|
3309
|
+
return false;
|
|
3310
|
+
}
|
|
3311
|
+
installedSource = config.source;
|
|
3312
|
+
return true;
|
|
3313
|
+
};
|
|
3314
|
+
if (!ensureInstalled()) {
|
|
3315
|
+
return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
|
|
3316
|
+
}
|
|
3317
|
+
const version = ports.headroomVersion() ?? "unknown";
|
|
3318
|
+
writeOwnState({ supervisorPid: ports.ownPid, version, installedSource, ...sticky() });
|
|
3319
|
+
ports.log(`agent-shim headroom supervisor ${String(ports.ownPid)}: managing headroom on allowlist [${allowlistOf(ports).join(", ")}]`);
|
|
3320
|
+
const orphanPid = previousState?.headroomPid;
|
|
3321
|
+
if (orphanPid !== void 0 && ports.isRunning(orphanPid)) {
|
|
3322
|
+
ports.log(`agent-shim headroom supervisor: stopping daemon pid ${String(orphanPid)} left behind by the previous supervisor`);
|
|
3323
|
+
ports.stopProcess(orphanPid);
|
|
3324
|
+
}
|
|
3325
|
+
let headroomPid;
|
|
3326
|
+
let runningHash;
|
|
3327
|
+
let runningSettingsHash;
|
|
3328
|
+
let consecutiveFailures = 0;
|
|
3329
|
+
let idleSince;
|
|
3330
|
+
let superseded = false;
|
|
3331
|
+
let ticks = 0;
|
|
3332
|
+
for (; ; ) {
|
|
3333
|
+
if (options.tickLimit !== void 0 && ticks >= options.tickLimit) {
|
|
3334
|
+
return HEADROOM_SUPERVISOR_STILL_RUNNING;
|
|
3335
|
+
}
|
|
3336
|
+
ticks += 1;
|
|
3337
|
+
config = ports.readConfig();
|
|
3338
|
+
const settingsHash = hashSettings(config.settings);
|
|
3339
|
+
const allowlist = allowlistOf(ports);
|
|
3340
|
+
const allowlistHash = hashAllowlist(allowlist);
|
|
3341
|
+
pruneDeadSessions(fs2, paths.headroomSessionsDir, ports.isRunning);
|
|
3342
|
+
const ownsNow = ownsState();
|
|
3343
|
+
if (!ownsNow && !superseded) {
|
|
3344
|
+
ports.log("agent-shim headroom supervisor: another supervisor now owns state.json; serving this daemon's own sessions until they end");
|
|
3345
|
+
}
|
|
3346
|
+
superseded = !ownsNow;
|
|
3347
|
+
const crashed = headroomPid !== void 0 && !ports.isRunning(headroomPid);
|
|
3348
|
+
if (headroomPid === void 0 || crashed) {
|
|
3349
|
+
if (crashed) {
|
|
3350
|
+
ports.log(`agent-shim headroom supervisor: headroom pid ${String(headroomPid)} died`);
|
|
3351
|
+
headroomPid = void 0;
|
|
3352
|
+
runningHash = void 0;
|
|
3353
|
+
runningSettingsHash = void 0;
|
|
3354
|
+
writeOwnState({
|
|
3355
|
+
supervisorPid: ports.ownPid,
|
|
3356
|
+
version,
|
|
3357
|
+
installedSource,
|
|
3358
|
+
...sticky()
|
|
3359
|
+
});
|
|
3360
|
+
}
|
|
3361
|
+
if (consecutiveFailures >= HEADROOM_START_RETRY_BUDGET) {
|
|
3362
|
+
return fail(`headroom failed to become ready ${String(consecutiveFailures)} times in a row; giving up`);
|
|
3363
|
+
}
|
|
3364
|
+
if (installedSource !== config.source && !ensureInstalled()) {
|
|
3365
|
+
return fail(`could not install headroom from ${config.source}: install failed (is uv installed and on PATH?)`);
|
|
3366
|
+
}
|
|
3367
|
+
const previousSticky = lastPort;
|
|
3368
|
+
const port = lastPort !== void 0 && await ports.isPortFree(lastPort) ? lastPort : await ports.freePort();
|
|
3369
|
+
const pid = ports.spawnHeadroom(port, allowlist, config.settings);
|
|
3370
|
+
const ready = await waitUntilReady(ports, port);
|
|
3371
|
+
if (ready) {
|
|
3372
|
+
consecutiveFailures = 0;
|
|
3373
|
+
headroomPid = pid;
|
|
3374
|
+
runningHash = allowlistHash;
|
|
3375
|
+
runningSettingsHash = settingsHash;
|
|
3376
|
+
lastPort = port;
|
|
3377
|
+
if (previousSticky !== void 0 && port !== previousSticky) {
|
|
3378
|
+
ports.log(
|
|
3379
|
+
`agent-shim headroom supervisor: sticky port ${String(previousSticky)} was occupied; moving to ${String(port)} (sessions launched against the old port are stale until they relaunch)`
|
|
3380
|
+
);
|
|
3381
|
+
}
|
|
3382
|
+
const recorded = writeOwnState({
|
|
3383
|
+
supervisorPid: ports.ownPid,
|
|
3384
|
+
headroomPid: pid,
|
|
3385
|
+
port,
|
|
3386
|
+
version,
|
|
3387
|
+
allowlistHash,
|
|
3388
|
+
settingsHash,
|
|
3389
|
+
installedSource: config.source,
|
|
3390
|
+
...sticky()
|
|
3391
|
+
});
|
|
3392
|
+
if (recorded) {
|
|
3393
|
+
fs2.removeRecursive(paths.headroomLockFile);
|
|
3394
|
+
}
|
|
3395
|
+
idleSince = void 0;
|
|
3396
|
+
ports.log(`agent-shim headroom supervisor: headroom pid ${String(pid)} ready on 127.0.0.1:${String(port)}`);
|
|
3397
|
+
} else {
|
|
3398
|
+
ports.stopProcess(pid);
|
|
3399
|
+
consecutiveFailures += 1;
|
|
3400
|
+
ports.log(
|
|
3401
|
+
`agent-shim headroom supervisor: headroom did not become ready on port ${String(port)} (attempt ${String(consecutiveFailures)} of ${String(HEADROOM_START_RETRY_BUDGET)})`
|
|
3402
|
+
);
|
|
3403
|
+
await ports.sleep(backoffForAttempt(consecutiveFailures));
|
|
3404
|
+
continue;
|
|
3405
|
+
}
|
|
3406
|
+
} else {
|
|
3407
|
+
if (runningHash !== allowlistHash || runningSettingsHash !== settingsHash || installedSource !== config.source) {
|
|
3408
|
+
if (sessionsForSupervisor(fs2, paths.headroomSessionsDir, ports.ownPid).length === 0) {
|
|
3409
|
+
ports.log("agent-shim headroom supervisor: configuration drifted and no sessions are live; restarting headroom");
|
|
3410
|
+
ports.stopProcess(headroomPid);
|
|
3411
|
+
headroomPid = void 0;
|
|
3412
|
+
runningHash = void 0;
|
|
3413
|
+
runningSettingsHash = void 0;
|
|
3414
|
+
continue;
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
const sessions = sessionsForSupervisor(fs2, paths.headroomSessionsDir, ports.ownPid);
|
|
3418
|
+
if (sessions.length === 0) {
|
|
3419
|
+
idleSince ??= ports.now();
|
|
3420
|
+
const idleMs = ports.now() - idleSince;
|
|
3421
|
+
if (idleMs >= config.idleShutdownMinutes * MS_PER_MINUTE2) {
|
|
3422
|
+
ports.log(
|
|
3423
|
+
`agent-shim headroom supervisor: no sessions for ${String(config.idleShutdownMinutes)} minute(s); stopping headroom and exiting`
|
|
3424
|
+
);
|
|
3425
|
+
ports.stopProcess(headroomPid);
|
|
3426
|
+
writeOwnState({
|
|
3427
|
+
version,
|
|
3428
|
+
...sticky()
|
|
3429
|
+
});
|
|
3430
|
+
return 0;
|
|
3431
|
+
}
|
|
3432
|
+
} else {
|
|
3433
|
+
idleSince = void 0;
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
await ports.sleep(HEADROOM_POLL_MS);
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
function currentHeadroomAllowlist(fs2, paths) {
|
|
3440
|
+
const providers = readAllProviders(fs2, paths.providersDir).map((entry) => entry.provider);
|
|
3441
|
+
return headroomAllowlist(headroomUpstreams(providers, frontDoorOrigin(readFrontDoorState(fs2, paths.frontdoorStateFile))));
|
|
3442
|
+
}
|
|
3443
|
+
function allowlistOf(ports) {
|
|
3444
|
+
return currentHeadroomAllowlist(ports.fs, ports.paths);
|
|
3445
|
+
}
|
|
3446
|
+
async function waitUntilReady(ports, port) {
|
|
3447
|
+
const deadline = ports.now() + HEADROOM_READY_TIMEOUT_MS;
|
|
3448
|
+
for (; ; ) {
|
|
3449
|
+
if (await ports.ready(port)) {
|
|
3450
|
+
return true;
|
|
3451
|
+
}
|
|
3452
|
+
if (ports.now() >= deadline) {
|
|
3453
|
+
return false;
|
|
3454
|
+
}
|
|
3455
|
+
await ports.sleep(HEADROOM_POLL_MS);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
// src/launcher/guard.ts
|
|
3460
|
+
var AMBIENT_CREDENTIAL_VARS = [
|
|
3461
|
+
"ANTHROPIC_API_KEY",
|
|
3462
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
3463
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
3464
|
+
"CLAUDE_CODE_USE_BEDROCK",
|
|
3465
|
+
"CLAUDE_CODE_USE_VERTEX",
|
|
3466
|
+
"CLAUDE_CODE_USE_FOUNDRY"
|
|
3467
|
+
];
|
|
3468
|
+
function detectAmbientCredential(env, injected) {
|
|
3469
|
+
for (const variable of AMBIENT_CREDENTIAL_VARS) {
|
|
3470
|
+
const value = env[variable];
|
|
3471
|
+
if (value === void 0 || value === "") {
|
|
3472
|
+
continue;
|
|
3473
|
+
}
|
|
3474
|
+
if (injected?.variable === variable && value === injected.token) {
|
|
3475
|
+
continue;
|
|
3476
|
+
}
|
|
3477
|
+
return { variable };
|
|
3478
|
+
}
|
|
3479
|
+
return void 0;
|
|
3480
|
+
}
|
|
3481
|
+
function formatAmbientCredentialGuardMessage(variable, identityName) {
|
|
3482
|
+
const identitySetCommand = identityName === void 0 ? "agent-shim identity set <name> --allow-ambient-credential" : `agent-shim identity set ${identityName} --allow-ambient-credential`;
|
|
3483
|
+
return [
|
|
3484
|
+
`error: ${variable} is set in the environment. This identity's isolated`,
|
|
3485
|
+
"credential would be bypassed \u2014 every identity authenticates as this same key",
|
|
3486
|
+
"while it's set. Unset it, or if this is deliberate, opt in per-launch with",
|
|
3487
|
+
"AGENT_SHIM_ALLOW_AMBIENT_CREDENTIAL=1, or persistently for this identity with",
|
|
3488
|
+
`\`${identitySetCommand}\`.`
|
|
3489
|
+
].join("\n");
|
|
3490
|
+
}
|
|
3491
|
+
function evaluateAmbientCredentialGuard(params) {
|
|
3492
|
+
const detected = detectAmbientCredential(params.env, params.injectedCredential);
|
|
3493
|
+
if (detected === void 0) {
|
|
3494
|
+
return { ok: true };
|
|
3495
|
+
}
|
|
3496
|
+
if (params.allowAmbientCredentialOverride || params.allowAmbientCredential || params.providerSelected === true) {
|
|
3497
|
+
return { ok: true };
|
|
3498
|
+
}
|
|
3499
|
+
return {
|
|
3500
|
+
ok: false,
|
|
3501
|
+
variable: detected.variable,
|
|
3502
|
+
message: formatAmbientCredentialGuardMessage(detected.variable, params.identityName)
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
export {
|
|
3506
|
+
AMBIENT_CREDENTIAL_VARS,
|
|
3507
|
+
CONNECT_INTERCEPT_HOST,
|
|
3508
|
+
CONNECT_LIMITS,
|
|
3509
|
+
FARM_MANIFEST_FILENAME,
|
|
3510
|
+
FrontDoorStartError,
|
|
3511
|
+
HeadroomStartError,
|
|
3512
|
+
ROUTED_PATH_PREFIX,
|
|
3513
|
+
UsageSnapshotError,
|
|
3514
|
+
UsageSnapshotSchema,
|
|
3515
|
+
buildEntryFacts,
|
|
3516
|
+
carryOver,
|
|
3517
|
+
createLeafCache,
|
|
3518
|
+
detectAmbientCredential,
|
|
3519
|
+
ensureCa,
|
|
3520
|
+
ensureFrontDoor,
|
|
3521
|
+
ensureHeadroom,
|
|
3522
|
+
evaluateAmbientCredentialGuard,
|
|
3523
|
+
formatAmbientCredentialGuardMessage,
|
|
3524
|
+
forwardableHeaders,
|
|
3525
|
+
generateCa,
|
|
3526
|
+
isInterceptedHost,
|
|
3527
|
+
listUsageSnapshots,
|
|
3528
|
+
mintLeaf,
|
|
3529
|
+
parseConnectTarget,
|
|
3530
|
+
readFarmManifest,
|
|
3531
|
+
readUsageSnapshot,
|
|
3532
|
+
realConnectCertStore,
|
|
3533
|
+
realConnectEffects,
|
|
3534
|
+
recoverFarm,
|
|
3535
|
+
recoveryDiagnostics,
|
|
3536
|
+
resolveDecisions,
|
|
3537
|
+
resolveSupervisorConfig,
|
|
3538
|
+
resyncFarm,
|
|
3539
|
+
runFrontDoorSupervisor,
|
|
3540
|
+
runSupervisor,
|
|
3541
|
+
servedByPipeline,
|
|
3542
|
+
snapshotPath,
|
|
3543
|
+
startConnectServer,
|
|
3544
|
+
topLevelNames
|
|
3545
|
+
};
|