@zgeoff/atc 2.36.0 → 2.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/agents/agent-adapter.ts +6 -1
- package/src/agents/build-agent-adapters.ts +32 -0
- package/src/agents/claude-adapter.ts +145 -65
- package/src/agents/codex-adapter.ts +17 -11
- package/src/agents/gateway-adapter.ts +17 -1
- package/src/agents/grok-adapter.ts +15 -9
- package/src/build-migrated-config.ts +184 -0
- package/src/cli.ts +49 -17
- package/src/client/collect-agent-picks.ts +3 -10
- package/src/client/index.ts +1 -1
- package/src/client/resolve-agent-metadata.ts +4 -10
- package/src/client/spawn-picker.ts +1 -2
- package/src/daemon/build-auth-binding.ts +2 -1
- package/src/daemon/daemon-connection.ts +1 -1
- package/src/daemon/daemon.ts +12 -3
- package/src/daemon/sessions.ts +8 -2
- package/src/mcp/build-spawn-descriptions.ts +2 -2
- package/src/run-config-migrate.ts +85 -0
- package/src/shared/check-gateway-auth.ts +173 -0
- package/src/shared/collect-agents.ts +326 -0
- package/src/shared/collect-claude-auth.ts +8 -6
- package/src/shared/collect-gateways.ts +2 -168
- package/src/shared/collect-legacy-agents.ts +77 -0
- package/src/shared/config.ts +70 -60
- package/src/shared/format-mixed-agent-keys.ts +10 -0
- package/src/shared/is-subscription-override-variable.ts +14 -0
- package/src/shared/legacy-agent-keys.ts +15 -0
- package/src/shared/outranking-variables.ts +17 -0
- package/src/shared/pick-default-agent.ts +12 -0
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { AgentID } from './agent-id';
|
|
3
3
|
import { buildOptionalStringArray } from './build-optional-string-array';
|
|
4
|
+
import { checkGatewayAuth } from './check-gateway-auth';
|
|
5
|
+
import type { GatewayAuth } from './check-gateway-auth';
|
|
4
6
|
import type { AuthProfile } from './collect-auth-profiles';
|
|
5
|
-
import { isBrokerVariable } from './is-broker-variable';
|
|
6
7
|
import { isRecord } from './report';
|
|
7
|
-
import { resolveAuthProfiles } from './resolve-auth-profiles';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The auth profiles a gateway's sessions are bound to through impd's
|
|
11
|
-
* broker, and the variables each session gets in place of a credential,
|
|
12
|
-
* every value the fixed placeholder.
|
|
13
|
-
*/
|
|
14
|
-
export interface GatewayAuth {
|
|
15
|
-
readonly profiles: readonly string[];
|
|
16
|
-
readonly placeholderEnv: Readonly<Record<string, string>>;
|
|
17
|
-
}
|
|
18
8
|
|
|
19
9
|
/**
|
|
20
10
|
* A Claude-compatible backend the Claude CLI is pointed at: its own agent id,
|
|
@@ -125,162 +115,6 @@ export function collectGateways(
|
|
|
125
115
|
return { gateways, errors };
|
|
126
116
|
}
|
|
127
117
|
|
|
128
|
-
interface GatewayEntry {
|
|
129
|
-
readonly baseURL?: string | undefined;
|
|
130
|
-
readonly apiKeyHelper?: string | undefined;
|
|
131
|
-
readonly env: Readonly<Record<string, string>>;
|
|
132
|
-
readonly settings?: Readonly<Record<string, unknown>> | undefined;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// The gateway's auth, or every problem that refuses it. The checks cover
|
|
136
|
-
// each place a session's environment comes from: the gateway env, the
|
|
137
|
-
// settings env, and the placeholders, and which of them would win.
|
|
138
|
-
function checkGatewayAuth(
|
|
139
|
-
raw: unknown,
|
|
140
|
-
entry: GatewayEntry,
|
|
141
|
-
authProfiles: ReadonlyMap<string, AuthProfile>,
|
|
142
|
-
): GatewayAuth | string[] {
|
|
143
|
-
const auth = parseGatewayAuth(raw);
|
|
144
|
-
|
|
145
|
-
if (typeof auth === 'string') {
|
|
146
|
-
return [auth];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const problems: string[] = [];
|
|
150
|
-
|
|
151
|
-
if (entry.apiKeyHelper !== undefined) {
|
|
152
|
-
problems.push(
|
|
153
|
-
'apiKeyHelper cannot be set together with auth, which supplies the credential through the broker',
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (entry.settings?.['apiKeyHelper'] !== undefined) {
|
|
158
|
-
problems.push(
|
|
159
|
-
'settings.apiKeyHelper cannot be set together with auth, which supplies the credential through the broker',
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const settingsEnv = toEnvKeys(entry.settings?.['env']);
|
|
164
|
-
|
|
165
|
-
problems.push(
|
|
166
|
-
...collectEnvProblems('env', Object.keys(entry.env)),
|
|
167
|
-
...collectEnvProblems('settings.env', settingsEnv),
|
|
168
|
-
...collectPlaceholderProblems(auth.placeholderEnv),
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
for (const key of Object.keys(auth.placeholderEnv)) {
|
|
172
|
-
for (const [source, keys] of [
|
|
173
|
-
['env', Object.keys(entry.env)],
|
|
174
|
-
['settings.env', settingsEnv],
|
|
175
|
-
] as const) {
|
|
176
|
-
if (keys.includes(key)) {
|
|
177
|
-
problems.push(`placeholderEnv.${key} is also set in ${source}, which would override it`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const resolution = resolveAuthProfiles(authProfiles, auth.profiles);
|
|
183
|
-
|
|
184
|
-
if ('problem' in resolution) {
|
|
185
|
-
problems.push(resolution.problem.message);
|
|
186
|
-
} else {
|
|
187
|
-
const hostProblem = findBaseURLProblem(entry.baseURL ?? '', resolution.resolved.hosts);
|
|
188
|
-
|
|
189
|
-
if (hostProblem !== null) {
|
|
190
|
-
problems.push(hostProblem);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return problems.length > 0 ? problems : auth;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// The value every placeholder variable holds; impd's broker swaps the real
|
|
198
|
-
// credential in on the host's side.
|
|
199
|
-
const PLACEHOLDER = 'imp-broker-placeholder';
|
|
200
|
-
|
|
201
|
-
function parseGatewayAuth(raw: unknown): GatewayAuth | string {
|
|
202
|
-
const profiles = isRecord(raw) ? raw['profiles'] : undefined;
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
!isRecord(raw) ||
|
|
206
|
-
!Array.isArray(profiles) ||
|
|
207
|
-
profiles.length === 0 ||
|
|
208
|
-
!profiles.every((name) => typeof name === 'string')
|
|
209
|
-
) {
|
|
210
|
-
return 'auth must be an object with a non-empty profiles array';
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const placeholderEnv = raw['placeholderEnv'] ?? {};
|
|
214
|
-
|
|
215
|
-
if (!isRecord(placeholderEnv) || Array.isArray(placeholderEnv)) {
|
|
216
|
-
return 'placeholderEnv must be an object of variable names';
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const env: Record<string, string> = {};
|
|
220
|
-
|
|
221
|
-
for (const [key, value] of Object.entries(placeholderEnv)) {
|
|
222
|
-
if (value !== PLACEHOLDER) {
|
|
223
|
-
return `placeholderEnv.${key} must be ${PLACEHOLDER}`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
env[key] = value;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return { profiles: profiles.map(String), placeholderEnv: env };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// A settings env's variable names; anything but an object sets none.
|
|
233
|
-
function toEnvKeys(value: unknown): string[] {
|
|
234
|
-
return isRecord(value) && !Array.isArray(value) ? Object.keys(value) : [];
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// Variables Claude reads its endpoint and credential from. Only the
|
|
238
|
-
// placeholders may set a credential variable, and the base URL comes from
|
|
239
|
-
// `baseURL` alone, whose host is checked against the profiles.
|
|
240
|
-
const CREDENTIAL_VARIABLES: ReadonlySet<string> = new Set([
|
|
241
|
-
'ANTHROPIC_AUTH_TOKEN',
|
|
242
|
-
'ANTHROPIC_API_KEY',
|
|
243
|
-
]);
|
|
244
|
-
|
|
245
|
-
const BASE_URL_VARIABLE = 'ANTHROPIC_BASE_URL';
|
|
246
|
-
|
|
247
|
-
function collectEnvProblems(source: string, keys: readonly string[]): string[] {
|
|
248
|
-
return keys
|
|
249
|
-
.filter(
|
|
250
|
-
(key) => isBrokerVariable(key) || CREDENTIAL_VARIABLES.has(key) || key === BASE_URL_VARIABLE,
|
|
251
|
-
)
|
|
252
|
-
.map((key) => `${source} must not set ${key}`);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function collectPlaceholderProblems(env: Readonly<Record<string, string>>): string[] {
|
|
256
|
-
return Object.keys(env)
|
|
257
|
-
.filter((key) => isBrokerVariable(key) || key === BASE_URL_VARIABLE)
|
|
258
|
-
.map((key) => `placeholderEnv must not set ${key}`);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// The broker matches a request by the exact host it connects to on the
|
|
262
|
-
// default https port, so the base URL must be https on that port, with a
|
|
263
|
-
// host one of the selected profiles covers.
|
|
264
|
-
function findBaseURLProblem(baseURL: string, hosts: readonly string[]): string | null {
|
|
265
|
-
let url: URL;
|
|
266
|
-
|
|
267
|
-
try {
|
|
268
|
-
url = new URL(baseURL);
|
|
269
|
-
} catch {
|
|
270
|
-
return 'baseURL must be an https URL with no port or user info';
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (url.protocol !== 'https:' || url.port !== '' || url.username !== '' || url.password !== '') {
|
|
274
|
-
return 'baseURL must be an https URL with no port or user info';
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (!hosts.includes(url.hostname)) {
|
|
278
|
-
return `baseURL host ${url.hostname} is not a host of the selected profiles (${hosts.join(', ')})`;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
118
|
// Claude Code settings the sessions of this gateway are started with, on top of
|
|
285
119
|
// the ones atc writes itself. Anything but an object is no extra settings.
|
|
286
120
|
function buildOptionalSettings() {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { AgentEntry } from './collect-agents';
|
|
2
|
+
import type { AuthProfile } from './collect-auth-profiles';
|
|
3
|
+
import { collectClaudeAuth } from './collect-claude-auth';
|
|
4
|
+
import { collectGateways } from './collect-gateways';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The agent keys of the old config.json shape, already defaulted: a binary and arguments per harness, and the raw `claudeAuth`
|
|
8
|
+
* and `gateways` values.
|
|
9
|
+
*/
|
|
10
|
+
interface LegacyAgentKeys {
|
|
11
|
+
readonly claudeBin: string;
|
|
12
|
+
readonly claudeArgs: readonly string[];
|
|
13
|
+
readonly claudeAuth: unknown;
|
|
14
|
+
readonly grokBin: string;
|
|
15
|
+
readonly grokArgs: readonly string[];
|
|
16
|
+
readonly codexBin: string;
|
|
17
|
+
readonly codexArgs: readonly string[];
|
|
18
|
+
readonly gateways: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface LegacyAgents {
|
|
22
|
+
readonly agents: AgentEntry[];
|
|
23
|
+
|
|
24
|
+
// The problems that left a Claude auth or a gateway out, in the text the
|
|
25
|
+
// old keys always reported.
|
|
26
|
+
readonly errors: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Translates the old agent keys to registry entries: `claude`, `grok`, and
|
|
31
|
+
* `codex` in that order, then each gateway the old parse accepts, in file
|
|
32
|
+
* order. The parse is the old lenient one, so an absent or wrong-typed field
|
|
33
|
+
* takes its default.
|
|
34
|
+
*/
|
|
35
|
+
export function collectLegacyAgents(
|
|
36
|
+
keys: LegacyAgentKeys,
|
|
37
|
+
authProfiles: ReadonlyMap<string, AuthProfile>,
|
|
38
|
+
): LegacyAgents {
|
|
39
|
+
const claudeAuth = collectClaudeAuth(keys.claudeAuth, authProfiles);
|
|
40
|
+
const gateways = collectGateways(keys.gateways, keys.claudeBin, keys.claudeArgs, authProfiles);
|
|
41
|
+
|
|
42
|
+
const agents: AgentEntry[] = [
|
|
43
|
+
{
|
|
44
|
+
id: 'claude',
|
|
45
|
+
kind: 'claude',
|
|
46
|
+
label: 'Claude',
|
|
47
|
+
mark: 'c',
|
|
48
|
+
bin: keys.claudeBin,
|
|
49
|
+
args: keys.claudeArgs,
|
|
50
|
+
env: {},
|
|
51
|
+
...(claudeAuth.auth === null
|
|
52
|
+
? {}
|
|
53
|
+
: { auth: { profiles: claudeAuth.auth.profiles, placeholderEnv: {} } }),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'grok',
|
|
57
|
+
kind: 'grok',
|
|
58
|
+
label: 'Grok',
|
|
59
|
+
mark: 'g',
|
|
60
|
+
bin: keys.grokBin,
|
|
61
|
+
args: keys.grokArgs,
|
|
62
|
+
env: {},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: 'codex',
|
|
66
|
+
kind: 'codex',
|
|
67
|
+
label: 'Codex',
|
|
68
|
+
mark: 'c',
|
|
69
|
+
bin: keys.codexBin,
|
|
70
|
+
args: keys.codexArgs,
|
|
71
|
+
env: {},
|
|
72
|
+
},
|
|
73
|
+
...gateways.gateways.map((gateway): AgentEntry => ({ ...gateway, kind: 'claude' })),
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
return { agents, errors: [...claudeAuth.errors, ...gateways.errors] };
|
|
77
|
+
}
|
package/src/shared/config.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import type { AgentID } from './agent-id';
|
|
4
5
|
import { buildOptionalBoolean } from './build-optional-boolean';
|
|
5
6
|
import { buildOptionalString } from './build-optional-string';
|
|
6
7
|
import { buildOptionalStringArray } from './build-optional-string-array';
|
|
8
|
+
import { collectAgents } from './collect-agents';
|
|
9
|
+
import type { AgentEntry } from './collect-agents';
|
|
7
10
|
import { collectAuthProfiles } from './collect-auth-profiles';
|
|
8
11
|
import type { AuthProfile } from './collect-auth-profiles';
|
|
9
|
-
import { collectClaudeAuth } from './collect-claude-auth';
|
|
10
|
-
import type { ClaudeAuth } from './collect-claude-auth';
|
|
11
12
|
import { collectDirRoots } from './collect-dir-roots';
|
|
12
|
-
import { collectGateways } from './collect-gateways';
|
|
13
|
-
import type { GatewayConfig } from './collect-gateways';
|
|
14
13
|
import { collectHooks } from './collect-hooks';
|
|
15
14
|
import type { HooksConfig } from './collect-hooks';
|
|
15
|
+
import { collectLegacyAgents } from './collect-legacy-agents';
|
|
16
16
|
import { collectPrincipals } from './collect-principals';
|
|
17
17
|
import { collectTargets } from './collect-targets';
|
|
18
18
|
import type { TargetConfig, TargetConfigError } from './collect-targets';
|
|
@@ -20,30 +20,28 @@ import { collectWorkspacesConfig } from './collect-workspaces-config';
|
|
|
20
20
|
import type { WorkspacesConfig } from './collect-workspaces-config';
|
|
21
21
|
import { DEFAULT_GIT_TRANSPORTS } from './default-git-transports';
|
|
22
22
|
import { formatJSONKind } from './format-json-kind';
|
|
23
|
+
import { formatMixedAgentKeys } from './format-mixed-agent-keys';
|
|
24
|
+
import { LEGACY_AGENT_KEYS } from './legacy-agent-keys';
|
|
25
|
+
import { pickDefaultAgent } from './pick-default-agent';
|
|
23
26
|
import { isRecord } from './report';
|
|
24
27
|
import { resolveHomeDir } from './resolve-home-dir';
|
|
25
28
|
|
|
26
29
|
export interface Config {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
codexArgs: string[];
|
|
30
|
+
// The agents atc offers, in menu order, and the problems that left an
|
|
31
|
+
// entry out.
|
|
32
|
+
agents: readonly AgentEntry[];
|
|
33
|
+
agentErrors: readonly string[];
|
|
34
|
+
|
|
35
|
+
// The old agent keys a file without `agents` sets, in file order. Empty
|
|
36
|
+
// for a file that uses `agents` or sets none of them.
|
|
37
|
+
legacyAgentKeys: readonly string[];
|
|
38
|
+
|
|
39
|
+
// The agent a spawn without one runs.
|
|
40
|
+
defaultAgent: AgentID;
|
|
39
41
|
dirs: DirsConfig;
|
|
40
42
|
workspaces: WorkspacesConfig;
|
|
41
|
-
gateways: GatewayConfig[];
|
|
42
|
-
|
|
43
|
-
// The problems that kept a gateway with auth out of the gateways.
|
|
44
|
-
gatewayErrors: readonly string[];
|
|
45
43
|
|
|
46
|
-
// The credential references
|
|
44
|
+
// The credential references an agent's auth selects from, by profile
|
|
47
45
|
// name, and the problems that kept a profile out.
|
|
48
46
|
authProfiles: ReadonlyMap<string, AuthProfile>;
|
|
49
47
|
authProfileErrors: readonly string[];
|
|
@@ -87,14 +85,20 @@ interface LeaderKey {
|
|
|
87
85
|
}
|
|
88
86
|
|
|
89
87
|
const DEFAULTS: Config = {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
88
|
+
agents: [
|
|
89
|
+
{
|
|
90
|
+
id: 'claude',
|
|
91
|
+
kind: 'claude',
|
|
92
|
+
label: 'Claude',
|
|
93
|
+
mark: 'c',
|
|
94
|
+
bin: 'claude',
|
|
95
|
+
args: [],
|
|
96
|
+
env: {},
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
agentErrors: [],
|
|
100
|
+
legacyAgentKeys: [],
|
|
101
|
+
defaultAgent: 'claude',
|
|
98
102
|
dirs: { roots: [] },
|
|
99
103
|
workspaces: {
|
|
100
104
|
githubOwner: null,
|
|
@@ -103,8 +107,6 @@ const DEFAULTS: Config = {
|
|
|
103
107
|
root: null,
|
|
104
108
|
targetRoots: new Map(),
|
|
105
109
|
},
|
|
106
|
-
gateways: [],
|
|
107
|
-
gatewayErrors: [],
|
|
108
110
|
authProfiles: new Map(),
|
|
109
111
|
authProfileErrors: [],
|
|
110
112
|
hooks: {},
|
|
@@ -139,6 +141,7 @@ export const daemonRecordFile = join(stateDir, 'daemon.json');
|
|
|
139
141
|
// field parses to undefined rather than failing the file, so a bad config
|
|
140
142
|
// falls back to a default instead of refusing to start atc.
|
|
141
143
|
const CONFIG_SCHEMA = z.object({
|
|
144
|
+
agents: z.unknown().optional(),
|
|
142
145
|
claudeBin: buildOptionalString(),
|
|
143
146
|
claudeArgs: buildOptionalStringArray(),
|
|
144
147
|
claudeAuth: z.unknown().optional(),
|
|
@@ -237,16 +240,17 @@ function tryWriteDefaultConfig(file: string): void {
|
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
/**
|
|
240
|
-
* The config.json text a first run writes
|
|
241
|
-
* principals, so the file holds the one implicit `local` target and no
|
|
243
|
+
* The config.json text a first run writes: the `claude` agent alone. It
|
|
244
|
+
* leaves out the targets and principals, so the file holds the one implicit `local` target and no
|
|
242
245
|
* principals until the user sets their own, no auth profiles, and the
|
|
243
246
|
* errors a parse reports, which belong to no file.
|
|
244
247
|
*/
|
|
245
248
|
export function renderDefaultConfig(): string {
|
|
246
249
|
const {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
+
agents: _agents,
|
|
251
|
+
agentErrors: _agentErrors,
|
|
252
|
+
legacyAgentKeys: _legacyAgentKeys,
|
|
253
|
+
defaultAgent: _defaultAgent,
|
|
250
254
|
authProfiles: _authProfiles,
|
|
251
255
|
authProfileErrors: _authProfileErrors,
|
|
252
256
|
targets: _targets,
|
|
@@ -264,7 +268,11 @@ export function renderDefaultConfig(): string {
|
|
|
264
268
|
const { targetRoots, ...rest } = workspaces;
|
|
265
269
|
|
|
266
270
|
return `${JSON.stringify(
|
|
267
|
-
{
|
|
271
|
+
{
|
|
272
|
+
agents: { claude: {} },
|
|
273
|
+
...written,
|
|
274
|
+
workspaces: { ...rest, targets: Object.fromEntries(targetRoots) },
|
|
275
|
+
},
|
|
268
276
|
null,
|
|
269
277
|
2,
|
|
270
278
|
)}\n`;
|
|
@@ -296,23 +304,31 @@ export function parseConfig(raw: unknown, file: string = configFile): Config {
|
|
|
296
304
|
);
|
|
297
305
|
}
|
|
298
306
|
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
307
|
+
const present = Object.keys(raw).filter((key) => LEGACY_AGENT_KEYS.includes(key));
|
|
308
|
+
|
|
309
|
+
if (Object.hasOwn(raw, 'agents') && present.length > 0) {
|
|
310
|
+
return buildUnusableConfig('config_malformed', file, formatMixedAgentKeys(present));
|
|
311
|
+
}
|
|
312
|
+
|
|
305
313
|
const dirs = { roots: collectDirRoots(parsed.data.dirs) };
|
|
306
314
|
const workspaces = collectWorkspacesConfig(parsed.data.workspaces);
|
|
307
315
|
const authProfiles = collectAuthProfiles(parsed.data.authProfiles);
|
|
308
|
-
const claudeAuth = collectClaudeAuth(parsed.data.claudeAuth, authProfiles.profiles);
|
|
309
316
|
|
|
310
|
-
const
|
|
311
|
-
parsed.data.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
317
|
+
const registry = Object.hasOwn(raw, 'agents')
|
|
318
|
+
? collectAgents(parsed.data.agents, authProfiles.profiles)
|
|
319
|
+
: collectLegacyAgents(
|
|
320
|
+
{
|
|
321
|
+
claudeBin: parsed.data.claudeBin ?? 'claude',
|
|
322
|
+
claudeArgs: parsed.data.claudeArgs ?? [],
|
|
323
|
+
claudeAuth: parsed.data.claudeAuth,
|
|
324
|
+
grokBin: parsed.data.grokBin ?? 'grok',
|
|
325
|
+
grokArgs: parsed.data.grokArgs ?? [],
|
|
326
|
+
codexBin: parsed.data.codexBin ?? 'codex',
|
|
327
|
+
codexArgs: parsed.data.codexArgs ?? [],
|
|
328
|
+
gateways: parsed.data.gateways,
|
|
329
|
+
},
|
|
330
|
+
authProfiles.profiles,
|
|
331
|
+
);
|
|
316
332
|
|
|
317
333
|
const hooks = collectHooks(parsed.data.hooks);
|
|
318
334
|
const targets = collectTargets(parsed.data.targets, parsed.data.defaultTarget);
|
|
@@ -322,18 +338,12 @@ export function parseConfig(raw: unknown, file: string = configFile): Config {
|
|
|
322
338
|
(parsed.data.leader === undefined ? null : decodeLeader(parsed.data.leader)) ?? DEFAULTS.leader;
|
|
323
339
|
|
|
324
340
|
return {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
grokBin,
|
|
330
|
-
grokArgs,
|
|
331
|
-
codexBin,
|
|
332
|
-
codexArgs,
|
|
341
|
+
agents: registry.agents,
|
|
342
|
+
agentErrors: registry.errors,
|
|
343
|
+
legacyAgentKeys: present,
|
|
344
|
+
defaultAgent: pickDefaultAgent(registry.agents),
|
|
333
345
|
dirs,
|
|
334
346
|
workspaces: workspaces.workspaces,
|
|
335
|
-
gateways: gateways.gateways,
|
|
336
|
-
gatewayErrors: gateways.errors,
|
|
337
347
|
authProfiles: authProfiles.profiles,
|
|
338
348
|
authProfileErrors: authProfiles.errors,
|
|
339
349
|
hooks,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The problem a config.json that sets both `agents` and old agent keys
|
|
3
|
+
* reports: the keys, as "a", "a and b", or "a, b and c", and where they go.
|
|
4
|
+
*/
|
|
5
|
+
export function formatMixedAgentKeys(keys: readonly string[]): string {
|
|
6
|
+
const last = keys.at(-1);
|
|
7
|
+
const list = keys.length < 2 ? keys.join('') : `${keys.slice(0, -1).join(', ')} and ${last}`;
|
|
8
|
+
|
|
9
|
+
return `${list} cannot be set together with agents; move them into agents or run 'atc config migrate'`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { isBrokerVariable } from './is-broker-variable';
|
|
2
|
+
import { OUTRANKING_VARIABLES } from './outranking-variables';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether a variable set in a Claude session's environment would override
|
|
6
|
+
* the subscription sign-in or route the CLI around impd's broker: an
|
|
7
|
+
* outranking credential, endpoint, or provider selector, the subscription
|
|
8
|
+
* token variable itself, or a proxy or CA variable.
|
|
9
|
+
*/
|
|
10
|
+
export function isSubscriptionOverrideVariable(key: string): boolean {
|
|
11
|
+
return OUTRANKING_VARIABLES.has(key) || key === OAUTH_VARIABLE || isBrokerVariable(key);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const OAUTH_VARIABLE = 'CLAUDE_CODE_OAUTH_TOKEN';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The config.json keys of the old agent shape, which `agents` replaces: the
|
|
3
|
+
* harness binaries and arguments, the Claude subscription auth, and the
|
|
4
|
+
* gateway map.
|
|
5
|
+
*/
|
|
6
|
+
export const LEGACY_AGENT_KEYS: readonly string[] = [
|
|
7
|
+
'claudeBin',
|
|
8
|
+
'claudeArgs',
|
|
9
|
+
'claudeAuth',
|
|
10
|
+
'grokBin',
|
|
11
|
+
'grokArgs',
|
|
12
|
+
'codexBin',
|
|
13
|
+
'codexArgs',
|
|
14
|
+
'gateways',
|
|
15
|
+
];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The variables that keep Claude Code from sending the subscription token
|
|
3
|
+
* to the Anthropic API: a credential it takes ahead of that token, another
|
|
4
|
+
* endpoint, or a cloud provider it signs in to instead.
|
|
5
|
+
*/
|
|
6
|
+
export const OUTRANKING_VARIABLES: ReadonlySet<string> = new Set([
|
|
7
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
8
|
+
'ANTHROPIC_API_KEY',
|
|
9
|
+
'ANTHROPIC_BASE_URL',
|
|
10
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
11
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
12
|
+
'CLAUDE_CODE_USE_FOUNDRY',
|
|
13
|
+
'CLAUDE_CODE_USE_MANTLE',
|
|
14
|
+
'CLAUDE_CODE_USE_ANTHROPIC_AWS',
|
|
15
|
+
'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
|
|
16
|
+
'CLAUDE_CODE_USE_GATEWAY',
|
|
17
|
+
]);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentID } from './agent-id';
|
|
2
|
+
import type { AgentEntry } from './collect-agents';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The agent a spawn without one runs: `claude` when the registry holds an
|
|
6
|
+
* entry with that id, else the first entry. An empty registry still reports
|
|
7
|
+
* `claude`, so a spawn is refused for want of an adapter rather than for an
|
|
8
|
+
* unknown default.
|
|
9
|
+
*/
|
|
10
|
+
export function pickDefaultAgent(agents: readonly AgentEntry[]): AgentID {
|
|
11
|
+
return agents.find((entry) => entry.id === 'claude')?.id ?? agents[0]?.id ?? 'claude';
|
|
12
|
+
}
|