@zgeoff/atc 2.40.0 → 2.41.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/package.json +1 -1
- package/src/agents/agent-adapter.ts +7 -2
- package/src/agents/claude-adapter.ts +6 -2
- package/src/agents/gateway-adapter.ts +29 -7
- package/src/daemon/build-auth-binding.ts +9 -4
- package/src/daemon/sessions.ts +7 -3
- package/src/shared/check-gateway-auth.ts +11 -0
- package/src/shared/collect-agents.ts +32 -0
- package/src/shared/collect-auth-profiles.ts +64 -1
- package/src/shared/collect-profile-env-problems.ts +16 -0
- package/src/shared/resolve-auth-profiles.ts +27 -2
package/package.json
CHANGED
|
@@ -44,12 +44,17 @@ export interface SpawnPlan {
|
|
|
44
44
|
* files unpack into. `auth` is given when the harness takes its credential
|
|
45
45
|
* from impd's broker: the revision of the host's runtime auth binding it
|
|
46
46
|
* launches under, which keys any settings the agent writes for it, and the
|
|
47
|
-
* placeholder variables the harness holds in place of a credential
|
|
47
|
+
* placeholder variables the harness holds in place of a credential, and
|
|
48
|
+
* the variables its auth profiles set.
|
|
48
49
|
*/
|
|
49
50
|
export interface GuestPaths {
|
|
50
51
|
readonly atc: string | null;
|
|
51
52
|
readonly dir: string;
|
|
52
|
-
readonly auth?: {
|
|
53
|
+
readonly auth?: {
|
|
54
|
+
readonly revision: number;
|
|
55
|
+
readonly env: Readonly<Record<string, string>>;
|
|
56
|
+
readonly profileEnv: Readonly<Record<string, string>>;
|
|
57
|
+
};
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
/**
|
|
@@ -290,7 +290,11 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
290
290
|
const padding = typeof userSettings === 'string' ? findStatuslinePadding(userSettings) : 0;
|
|
291
291
|
|
|
292
292
|
const settings = buildHookSettings(
|
|
293
|
-
this.buildSettingsProfile({
|
|
293
|
+
this.buildSettingsProfile({
|
|
294
|
+
...auth.profileEnv,
|
|
295
|
+
...auth.env,
|
|
296
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
297
|
+
}),
|
|
294
298
|
padding,
|
|
295
299
|
argv,
|
|
296
300
|
);
|
|
@@ -312,7 +316,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
312
316
|
),
|
|
313
317
|
...Object.fromEntries(bridge),
|
|
314
318
|
},
|
|
315
|
-
env: { ...launch.env, ...auth.env },
|
|
319
|
+
env: { ...launch.env, ...auth.profileEnv, ...auth.env },
|
|
316
320
|
};
|
|
317
321
|
}
|
|
318
322
|
|
|
@@ -224,6 +224,7 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
224
224
|
env: {
|
|
225
225
|
...this.gateway.env,
|
|
226
226
|
ANTHROPIC_BASE_URL: this.gateway.baseURL,
|
|
227
|
+
...guest.auth.profileEnv,
|
|
227
228
|
...guest.auth.env,
|
|
228
229
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
229
230
|
},
|
|
@@ -253,7 +254,7 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
253
254
|
...buildClaudeConfigSeed(null),
|
|
254
255
|
...Object.fromEntries(bridge),
|
|
255
256
|
},
|
|
256
|
-
env: { ...launch.env, ...guest.auth.env },
|
|
257
|
+
env: { ...launch.env, ...guest.auth.profileEnv, ...guest.auth.env },
|
|
257
258
|
};
|
|
258
259
|
}
|
|
259
260
|
|
|
@@ -352,19 +353,31 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
352
353
|
}
|
|
353
354
|
|
|
354
355
|
// The Claude CLI sends ANTHROPIC_AUTH_TOKEN as a bearer authorization
|
|
355
|
-
// header, the one pairing atc binds, so the placeholders must
|
|
356
|
-
// variable
|
|
357
|
-
//
|
|
358
|
-
//
|
|
356
|
+
// header, the one pairing atc binds, so the placeholders must include that
|
|
357
|
+
// variable, holding the placeholder, for a profile whose rule on the base
|
|
358
|
+
// URL's host sets that header. Any other variable passes through to the
|
|
359
|
+
// session for a tool in it, whose own host a selected profile covers,
|
|
360
|
+
// except a variable the CLI reads as its own credential, which would
|
|
361
|
+
// compete with the bearer variable and put the placeholder in a header
|
|
362
|
+
// the broker never fills, and the variable that names the session's
|
|
363
|
+
// Claude config folder, which atc sets itself.
|
|
359
364
|
private findPlaceholderRefusal(
|
|
360
365
|
env: Readonly<Record<string, string>>,
|
|
361
366
|
profiles: readonly string[],
|
|
362
367
|
): DaemonError | null {
|
|
363
368
|
const keys = Object.keys(env);
|
|
364
369
|
|
|
365
|
-
if (keys.
|
|
370
|
+
if (!keys.includes(BEARER_VARIABLE)) {
|
|
366
371
|
return this.buildPlaceholderRefusal(
|
|
367
|
-
`needs ${BEARER_VARIABLE}
|
|
372
|
+
`needs ${BEARER_VARIABLE} among its placeholder variables, which the broker fills as a bearer authorization header; it has ${keys.length === 0 ? 'none' : keys.join(', ')}`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const competing = keys.find((key) => COMPETING_CREDENTIAL_VARIABLES.has(key));
|
|
377
|
+
|
|
378
|
+
if (competing !== undefined) {
|
|
379
|
+
return this.buildPlaceholderRefusal(
|
|
380
|
+
`cannot use ${competing} as a placeholder variable, since the Claude CLI reads it as its own credential or config folder beside ${BEARER_VARIABLE}`,
|
|
368
381
|
);
|
|
369
382
|
}
|
|
370
383
|
|
|
@@ -444,6 +457,15 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
444
457
|
const BEARER_VARIABLE = 'ANTHROPIC_AUTH_TOKEN';
|
|
445
458
|
const PLACEHOLDER = 'imp-broker-placeholder';
|
|
446
459
|
|
|
460
|
+
// Variables the Claude CLI reads as its own credential beside the bearer
|
|
461
|
+
// variable, plus the one that points it at the session's config folder,
|
|
462
|
+
// whose launch value a placeholder would overwrite.
|
|
463
|
+
const COMPETING_CREDENTIAL_VARIABLES: ReadonlySet<string> = new Set([
|
|
464
|
+
'ANTHROPIC_API_KEY',
|
|
465
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
466
|
+
'CLAUDE_CONFIG_DIR',
|
|
467
|
+
]);
|
|
468
|
+
|
|
447
469
|
// The Claude CLI's mode that asks a person before each action.
|
|
448
470
|
const MANUAL_PERMISSION_MODE = 'default';
|
|
449
471
|
|
|
@@ -11,8 +11,9 @@ import { sortJSONKeys } from '../shared/sort-json-keys';
|
|
|
11
11
|
* What a session's runtime is bound to through impd's broker: every
|
|
12
12
|
* profile its gateway's selection reaches, the secrets they need impd to
|
|
13
13
|
* hold with exactly these rules, the placeholder variables in place of a
|
|
14
|
-
* credential, and the endpoint. `hash`
|
|
15
|
-
*
|
|
14
|
+
* credential, the variables the profiles set, and the endpoint. `hash`
|
|
15
|
+
* covers the secrets and their rules, and the profile variables when there
|
|
16
|
+
* are any, so a change to a profile the binding does not reach leaves it as
|
|
16
17
|
* it was, and a change to one it reaches gives another hash.
|
|
17
18
|
*/
|
|
18
19
|
export interface AuthBinding {
|
|
@@ -21,6 +22,7 @@ export interface AuthBinding {
|
|
|
21
22
|
readonly profiles: readonly string[];
|
|
22
23
|
readonly secrets: readonly ResolvedAuthSecret[];
|
|
23
24
|
readonly placeholderEnv: Readonly<Record<string, string>>;
|
|
25
|
+
readonly profileEnv: Readonly<Record<string, string>>;
|
|
24
26
|
readonly hash: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -29,7 +31,7 @@ type AuthGateway = Pick<GatewayConfig, 'id' | 'baseURL'> & { readonly auth: Gate
|
|
|
29
31
|
/**
|
|
30
32
|
* Plans the binding a gateway's auth asks for against the current auth
|
|
31
33
|
* profiles, or the problem that refuses it. The hash is the SHA-256 of the
|
|
32
|
-
* resolved secrets as canonical JSON, with each list sorted, so the order
|
|
34
|
+
* resolved secrets (and profile variables, when set) as canonical JSON, with each list sorted, so the order
|
|
33
35
|
* of the selection or of the config never changes it.
|
|
34
36
|
*/
|
|
35
37
|
export function buildAuthBinding(
|
|
@@ -43,6 +45,8 @@ export function buildAuthBinding(
|
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
const secrets = resolution.resolved.secrets;
|
|
48
|
+
const profileEnv = resolution.resolved.env;
|
|
49
|
+
const hashed = Object.keys(profileEnv).length === 0 ? { secrets } : { secrets, profileEnv };
|
|
46
50
|
|
|
47
51
|
return {
|
|
48
52
|
binding: {
|
|
@@ -51,8 +55,9 @@ export function buildAuthBinding(
|
|
|
51
55
|
profiles: resolution.resolved.profiles,
|
|
52
56
|
secrets,
|
|
53
57
|
placeholderEnv: gateway.auth.placeholderEnv,
|
|
58
|
+
profileEnv,
|
|
54
59
|
hash: createHash('sha256')
|
|
55
|
-
.update(JSON.stringify(sortJSONKeys(
|
|
60
|
+
.update(JSON.stringify(sortJSONKeys(hashed)))
|
|
56
61
|
.digest('hex'),
|
|
57
62
|
},
|
|
58
63
|
};
|
package/src/daemon/sessions.ts
CHANGED
|
@@ -2014,7 +2014,7 @@ export class SessionManager {
|
|
|
2014
2014
|
: {
|
|
2015
2015
|
atc: guest.atc,
|
|
2016
2016
|
dir,
|
|
2017
|
-
auth: await this.planGuestAuth(hostKey, auth.mode, auth.binding
|
|
2017
|
+
auth: await this.planGuestAuth(hostKey, auth.mode, auth.binding),
|
|
2018
2018
|
};
|
|
2019
2019
|
|
|
2020
2020
|
const plan =
|
|
@@ -2100,11 +2100,15 @@ export class SessionManager {
|
|
|
2100
2100
|
private async planGuestAuth(
|
|
2101
2101
|
hostKey: SessionID,
|
|
2102
2102
|
mode: HarnessAuthSetup['mode'],
|
|
2103
|
-
|
|
2103
|
+
binding: Pick<AuthBinding, 'placeholderEnv' | 'profileEnv'>,
|
|
2104
2104
|
): Promise<NonNullable<GuestPaths['auth']>> {
|
|
2105
2105
|
const held = mode === 'create' ? null : await this.requireAuthBinder().findBinding(hostKey);
|
|
2106
2106
|
|
|
2107
|
-
return {
|
|
2107
|
+
return {
|
|
2108
|
+
revision: held?.revision ?? 1,
|
|
2109
|
+
env: binding.placeholderEnv,
|
|
2110
|
+
profileEnv: binding.profileEnv,
|
|
2111
|
+
};
|
|
2108
2112
|
}
|
|
2109
2113
|
|
|
2110
2114
|
// Creates the binding of a host a spawn provisions, or verifies the one
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AuthProfile } from './collect-auth-profiles';
|
|
2
|
+
import { collectProfileEnvProblems } from './collect-profile-env-problems';
|
|
2
3
|
import { isBrokerVariable } from './is-broker-variable';
|
|
3
4
|
import { isRecord } from './report';
|
|
4
5
|
import { resolveAuthProfiles } from './resolve-auth-profiles';
|
|
@@ -75,6 +76,16 @@ export function checkGatewayAuth(
|
|
|
75
76
|
if ('problem' in resolution) {
|
|
76
77
|
problems.push(resolution.problem.message);
|
|
77
78
|
} else {
|
|
79
|
+
problems.push(
|
|
80
|
+
...collectProfileEnvProblems(
|
|
81
|
+
[
|
|
82
|
+
['env', Object.keys(entry.env)],
|
|
83
|
+
['settings.env', settingsEnv],
|
|
84
|
+
],
|
|
85
|
+
resolution.resolved.envOwners,
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
|
|
78
89
|
const hostProblem = findBaseURLProblem(entry.baseURL ?? '', resolution.resolved.hosts);
|
|
79
90
|
|
|
80
91
|
if (hostProblem !== null) {
|
|
@@ -4,8 +4,10 @@ import type { GatewayAuth } from './check-gateway-auth';
|
|
|
4
4
|
import type { AuthProfile } from './collect-auth-profiles';
|
|
5
5
|
import { collectClaudeAuth } from './collect-claude-auth';
|
|
6
6
|
import type { ClaudeMCPServer } from './collect-claude-auth';
|
|
7
|
+
import { collectProfileEnvProblems } from './collect-profile-env-problems';
|
|
7
8
|
import { isSubscriptionOverrideVariable } from './is-subscription-override-variable';
|
|
8
9
|
import { isRecord } from './report';
|
|
10
|
+
import { resolveAuthProfiles } from './resolve-auth-profiles';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* The agent CLI an entry drives, which decides the adapter behind it.
|
|
@@ -305,6 +307,10 @@ function readClaudeAuth(
|
|
|
305
307
|
|
|
306
308
|
problems.push(...collectSubscriptionProblems(entry));
|
|
307
309
|
|
|
310
|
+
if (collected.auth !== null) {
|
|
311
|
+
problems.push(...collectEntryProfileEnvProblems(entry, collected.auth.profiles, authProfiles));
|
|
312
|
+
}
|
|
313
|
+
|
|
308
314
|
if (collected.auth === null) {
|
|
309
315
|
return { problems: [...collected.errors, ...problems] };
|
|
310
316
|
}
|
|
@@ -320,6 +326,32 @@ function readClaudeAuth(
|
|
|
320
326
|
};
|
|
321
327
|
}
|
|
322
328
|
|
|
329
|
+
// The variables a stock entry sets that its selected profiles also set.
|
|
330
|
+
function collectEntryProfileEnvProblems(
|
|
331
|
+
entry: AgentEntry,
|
|
332
|
+
selected: readonly string[],
|
|
333
|
+
authProfiles: ReadonlyMap<string, AuthProfile>,
|
|
334
|
+
): string[] {
|
|
335
|
+
const resolution = resolveAuthProfiles(authProfiles, selected);
|
|
336
|
+
|
|
337
|
+
if ('problem' in resolution) {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const settingsEnv = entry.settings?.['env'];
|
|
342
|
+
|
|
343
|
+
return collectProfileEnvProblems(
|
|
344
|
+
[
|
|
345
|
+
['env', Object.keys(entry.env)],
|
|
346
|
+
[
|
|
347
|
+
'settings.env',
|
|
348
|
+
isRecord(settingsEnv) && !Array.isArray(settingsEnv) ? Object.keys(settingsEnv) : [],
|
|
349
|
+
],
|
|
350
|
+
],
|
|
351
|
+
resolution.resolved.envOwners,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
323
355
|
// What a stock entry with `auth` sets that would override the subscription
|
|
324
356
|
// sign-in or route the CLI around impd's broker.
|
|
325
357
|
function collectSubscriptionProblems(entry: AgentEntry): string[] {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isBrokerVariable } from './is-broker-variable';
|
|
2
|
+
import { isSubscriptionOverrideVariable } from './is-subscription-override-variable';
|
|
1
3
|
import { isRecord } from './report';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -16,6 +18,7 @@ interface CustomAuthProfile {
|
|
|
16
18
|
readonly host: string;
|
|
17
19
|
readonly header: string;
|
|
18
20
|
readonly scheme: 'bearer';
|
|
21
|
+
readonly env: Readonly<Record<string, string>>;
|
|
19
22
|
readonly dependencies: readonly string[];
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -23,6 +26,7 @@ interface GitHubAuthProfile {
|
|
|
23
26
|
readonly name: string;
|
|
24
27
|
readonly secret: string;
|
|
25
28
|
readonly kind: 'github';
|
|
29
|
+
readonly env: Readonly<Record<string, string>>;
|
|
26
30
|
readonly dependencies: readonly string[];
|
|
27
31
|
}
|
|
28
32
|
|
|
@@ -112,7 +116,11 @@ function parseAuthProfile(name: string, entry: unknown): AuthProfile | string {
|
|
|
112
116
|
return `${extra} cannot be set on a github profile, whose hosts and headers impd's github kind fixes`;
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
|
|
119
|
+
if (entry['env'] !== undefined) {
|
|
120
|
+
return "env cannot be set on a github profile, whose placeholders impd's github kind sets";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { name, secret, kind, env: {}, dependencies: dependencies ?? [] };
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
if (typeof host !== 'string' || host.length > BROKER_HOST_MAX || !BROKER_HOST.test(host)) {
|
|
@@ -131,6 +139,12 @@ function parseAuthProfile(name: string, entry: unknown): AuthProfile | string {
|
|
|
131
139
|
return 'user pairs only with the basic scheme, which atc does not bind';
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
const env = parseProfileEnv(entry['env'], host);
|
|
143
|
+
|
|
144
|
+
if (typeof env === 'string') {
|
|
145
|
+
return env;
|
|
146
|
+
}
|
|
147
|
+
|
|
134
148
|
return {
|
|
135
149
|
name,
|
|
136
150
|
secret,
|
|
@@ -138,6 +152,55 @@ function parseAuthProfile(name: string, entry: unknown): AuthProfile | string {
|
|
|
138
152
|
host,
|
|
139
153
|
header,
|
|
140
154
|
scheme,
|
|
155
|
+
env,
|
|
141
156
|
dependencies: dependencies ?? [],
|
|
142
157
|
};
|
|
143
158
|
}
|
|
159
|
+
|
|
160
|
+
// The value a profile's variable may hold: the placeholder the broker swaps
|
|
161
|
+
// a credential in for, or the profile's own host as an https origin. No
|
|
162
|
+
// other value, so a credential is never written into a profile.
|
|
163
|
+
const ENV_PLACEHOLDER = 'imp-broker-placeholder';
|
|
164
|
+
const ENV_NAME = /^[A-Z_][A-Z0-9_]*$/;
|
|
165
|
+
const RESERVED_ENV_PREFIXES = ['ANTHROPIC_', 'CLAUDE_', 'ATC_'];
|
|
166
|
+
|
|
167
|
+
const RESERVED_ENV_NAMES: ReadonlySet<string> = new Set(['PATH', 'HOME']);
|
|
168
|
+
|
|
169
|
+
function parseProfileEnv(raw: unknown, host: string): Record<string, string> | string {
|
|
170
|
+
if (raw === undefined) {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!isRecord(raw) || Array.isArray(raw)) {
|
|
175
|
+
return 'env must be an object of variable names';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const env: Record<string, string> = {};
|
|
179
|
+
|
|
180
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
181
|
+
if (!ENV_NAME.test(key)) {
|
|
182
|
+
return `env.${key} is not a variable name: use capital letters, digits and underscores`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (isReservedEnvName(key)) {
|
|
186
|
+
return `env.${key} cannot be set: atc or impd sets or reserves it`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (value !== ENV_PLACEHOLDER && value !== `https://${host}`) {
|
|
190
|
+
return `env.${key} must be ${ENV_PLACEHOLDER} or https://${host}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
env[key] = value;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return env;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isReservedEnvName(key: string): boolean {
|
|
200
|
+
return (
|
|
201
|
+
isBrokerVariable(key) ||
|
|
202
|
+
isSubscriptionOverrideVariable(key) ||
|
|
203
|
+
RESERVED_ENV_NAMES.has(key) ||
|
|
204
|
+
RESERVED_ENV_PREFIXES.some((prefix) => key.startsWith(prefix))
|
|
205
|
+
);
|
|
206
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every variable an entry's `env` or `settings.env` sets that one of its
|
|
3
|
+
* selected auth profiles also sets, which would win over or lose to the
|
|
4
|
+
* profile's value by order. `owners` maps each profile variable to the
|
|
5
|
+
* profile that sets it.
|
|
6
|
+
*/
|
|
7
|
+
export function collectProfileEnvProblems(
|
|
8
|
+
sources: readonly (readonly [string, readonly string[]])[],
|
|
9
|
+
owners: Readonly<Record<string, string>>,
|
|
10
|
+
): string[] {
|
|
11
|
+
return sources.flatMap(([source, keys]) =>
|
|
12
|
+
keys
|
|
13
|
+
.filter((key) => owners[key] !== undefined)
|
|
14
|
+
.map((key) => `${source} sets ${key}, which auth profile ${owners[key]} sets`),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -23,7 +23,8 @@ export interface ResolvedAuthSecret {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* A selection with its dependencies expanded: every profile it reaches,
|
|
26
|
-
* every host those profiles send a credential to,
|
|
26
|
+
* every host those profiles send a credential to, the variables they set
|
|
27
|
+
* in the guest with the profile each came from, and the rules grouped by
|
|
27
28
|
* secret, each list sorted so two selections of the same profiles resolve
|
|
28
29
|
* the same.
|
|
29
30
|
*/
|
|
@@ -31,6 +32,8 @@ interface ResolvedAuthProfiles {
|
|
|
31
32
|
readonly profiles: readonly string[];
|
|
32
33
|
readonly hosts: readonly string[];
|
|
33
34
|
readonly secrets: readonly ResolvedAuthSecret[];
|
|
35
|
+
readonly env: Readonly<Record<string, string>>;
|
|
36
|
+
readonly envOwners: Readonly<Record<string, string>>;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/**
|
|
@@ -40,7 +43,8 @@ interface ResolvedAuthProfiles {
|
|
|
40
43
|
* the config refused.
|
|
41
44
|
* - `auth_dependency_cycle`: the dependencies loop back on themselves.
|
|
42
45
|
* - `auth_collision`: two profiles send different credentials or rules to
|
|
43
|
-
* one host, which impd could not tell apart
|
|
46
|
+
* one host, which impd could not tell apart, or set one variable to
|
|
47
|
+
* different values.
|
|
44
48
|
*/
|
|
45
49
|
export interface AuthProfileProblem {
|
|
46
50
|
readonly code: 'auth_profile_unknown' | 'auth_dependency_cycle' | 'auth_collision';
|
|
@@ -85,6 +89,9 @@ export function resolveAuthProfiles(
|
|
|
85
89
|
const byHost = new Map<string, ProfileRule>();
|
|
86
90
|
const kinds = new Map<string, AuthProfile>();
|
|
87
91
|
|
|
92
|
+
const env: Record<string, string> = {};
|
|
93
|
+
const envOwners: Record<string, string> = {};
|
|
94
|
+
|
|
88
95
|
for (const profile of ordered) {
|
|
89
96
|
const kindOwner = kinds.get(profile.secret);
|
|
90
97
|
|
|
@@ -99,6 +106,22 @@ export function resolveAuthProfiles(
|
|
|
99
106
|
};
|
|
100
107
|
}
|
|
101
108
|
|
|
109
|
+
for (const [key, value] of Object.entries(profile.env)) {
|
|
110
|
+
const owner = envOwners[key];
|
|
111
|
+
|
|
112
|
+
if (owner === undefined) {
|
|
113
|
+
env[key] = value;
|
|
114
|
+
envOwners[key] = profile.name;
|
|
115
|
+
} else if (env[key] !== value) {
|
|
116
|
+
return {
|
|
117
|
+
problem: {
|
|
118
|
+
code: 'auth_collision',
|
|
119
|
+
message: `profiles ${owner} and ${profile.name} set ${key} to different values`,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
for (const rule of getProfileRules(profile)) {
|
|
103
126
|
const other = byHost.get(rule.host);
|
|
104
127
|
|
|
@@ -120,6 +143,8 @@ export function resolveAuthProfiles(
|
|
|
120
143
|
profiles: ordered.map((profile) => profile.name),
|
|
121
144
|
hosts: [...byHost.keys()].toSorted(),
|
|
122
145
|
secrets: buildSecrets([...byHost.values()]),
|
|
146
|
+
env,
|
|
147
|
+
envOwners,
|
|
123
148
|
},
|
|
124
149
|
};
|
|
125
150
|
}
|