@zgeoff/atc 2.36.1 → 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
package/README.md
CHANGED
|
@@ -151,7 +151,8 @@ installs in detail.
|
|
|
151
151
|
|
|
152
152
|
atc creates `~/.config/atc/config.json` on first run. The
|
|
153
153
|
[configuration guide](./docs/guides/configuration.md) covers every field: agent binaries and
|
|
154
|
-
arguments, the leader key, gateways, and hooks
|
|
154
|
+
arguments, the leader key, gateways, and hooks, and how `atc config migrate` moves an old config to
|
|
155
|
+
the `agents` map.
|
|
155
156
|
|
|
156
157
|
## Documentation
|
|
157
158
|
|
package/package.json
CHANGED
|
@@ -3,8 +3,9 @@ import type { DaemonError } from '../protocol/daemon-error';
|
|
|
3
3
|
import type { HookEvent } from '../protocol/hook-event';
|
|
4
4
|
import type { AgentID } from '../shared/agent-id';
|
|
5
5
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
6
|
+
import type { GatewayAuth } from '../shared/check-gateway-auth';
|
|
6
7
|
import type { AuthProfile } from '../shared/collect-auth-profiles';
|
|
7
|
-
import type {
|
|
8
|
+
import type { GatewayConfig } from '../shared/collect-gateways';
|
|
8
9
|
import type { SessionID } from '../shared/session-id';
|
|
9
10
|
|
|
10
11
|
export interface SpawnOptions {
|
|
@@ -221,6 +222,10 @@ export interface AgentAdapter {
|
|
|
221
222
|
// registered adapters.
|
|
222
223
|
readonly id: AgentID;
|
|
223
224
|
|
|
225
|
+
// The agent value this harness's own hook lines carry, which a hook line
|
|
226
|
+
// must match for the session to act on it. Absent: the adapter's id.
|
|
227
|
+
readonly hookAgent?: AgentID;
|
|
228
|
+
|
|
224
229
|
// Runs one headless turn over a session; null means eject is unsupported
|
|
225
230
|
// for this agent.
|
|
226
231
|
readonly headlessRunner: HeadlessRunner | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Config } from '../shared/config';
|
|
2
|
+
import type { AgentAdapter } from './agent-adapter';
|
|
3
|
+
import { ClaudeAdapter } from './claude-adapter';
|
|
4
|
+
import { CodexAdapter } from './codex-adapter';
|
|
5
|
+
import { GatewayAdapter } from './gateway-adapter';
|
|
6
|
+
import { GrokAdapter } from './grok-adapter';
|
|
7
|
+
import type { ClaudeHeadlessRun } from './make-claude-headless-runner';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One adapter per registry entry, in registry order. A Claude entry with a
|
|
11
|
+
* base URL is a gateway, and one without is stock Claude.
|
|
12
|
+
*/
|
|
13
|
+
export function buildAgentAdapters(
|
|
14
|
+
config: Config,
|
|
15
|
+
headlessRun: ClaudeHeadlessRun | null = null,
|
|
16
|
+
): AgentAdapter[] {
|
|
17
|
+
return config.agents.map((entry): AgentAdapter => {
|
|
18
|
+
if (entry.kind === 'codex') {
|
|
19
|
+
return new CodexAdapter(entry);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (entry.kind === 'grok') {
|
|
23
|
+
return new GrokAdapter(entry);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (entry.baseURL === undefined) {
|
|
27
|
+
return new ClaudeAdapter(entry, config, headlessRun);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return new GatewayAdapter({ ...entry, baseURL: entry.baseURL }, config, headlessRun);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -4,10 +4,13 @@ import { z } from 'zod';
|
|
|
4
4
|
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
5
5
|
import { DaemonError } from '../protocol/daemon-error';
|
|
6
6
|
import type { HookEvent } from '../protocol/hook-event';
|
|
7
|
+
import type { AgentID } from '../shared/agent-id';
|
|
7
8
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
8
9
|
import { buildOptionalString } from '../shared/build-optional-string';
|
|
10
|
+
import type { AgentEntry } from '../shared/collect-agents';
|
|
9
11
|
import type { Config } from '../shared/config';
|
|
10
|
-
import {
|
|
12
|
+
import { isSubscriptionOverrideVariable } from '../shared/is-subscription-override-variable';
|
|
13
|
+
import { OUTRANKING_VARIABLES } from '../shared/outranking-variables';
|
|
11
14
|
import { isRecord } from '../shared/report';
|
|
12
15
|
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
13
16
|
import { toShellArg } from '../shared/to-shell-arg';
|
|
@@ -31,6 +34,8 @@ import { buildClaudeConfigSeed } from './build-claude-config-seed';
|
|
|
31
34
|
import { buildClaudeGuestLaunch } from './build-claude-guest-launch';
|
|
32
35
|
import { buildClaudeOverrideArgs } from './build-claude-override-args';
|
|
33
36
|
import { buildHookSettings } from './build-hook-settings';
|
|
37
|
+
import type { HookSettingsProfile } from './build-hook-settings';
|
|
38
|
+
import { buildRestoreModeArgs } from './build-restore-mode-args';
|
|
34
39
|
import { CLAUDE_CONFIG_BUNDLE_FOLDER } from './claude-config-bundle-folder';
|
|
35
40
|
import { CLAUDE_EFFORT_LEVELS } from './claude-effort-levels';
|
|
36
41
|
import { findClaudePermissionMode } from './find-claude-permission-mode';
|
|
@@ -65,7 +70,11 @@ type ClaudeHookPayload = z.infer<typeof CLAUDE_HOOK_PAYLOAD_SCHEMA>;
|
|
|
65
70
|
* resume semantics, transcript name-pulling, and statusline chaining.
|
|
66
71
|
*/
|
|
67
72
|
export class ClaudeAdapter implements AgentAdapter {
|
|
68
|
-
readonly id
|
|
73
|
+
readonly id: AgentID;
|
|
74
|
+
|
|
75
|
+
// The agent value the hook lines of a Claude session carry: the settings
|
|
76
|
+
// file atc writes for this id names it on each hook command.
|
|
77
|
+
readonly hookAgent: AgentID;
|
|
69
78
|
|
|
70
79
|
readonly headlessRunner: HeadlessRunner | null;
|
|
71
80
|
|
|
@@ -82,7 +91,9 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
82
91
|
|
|
83
92
|
readonly takesMessages = true;
|
|
84
93
|
|
|
85
|
-
private readonly
|
|
94
|
+
private readonly entry: AgentEntry;
|
|
95
|
+
|
|
96
|
+
private readonly authProfiles: Config['authProfiles'];
|
|
86
97
|
|
|
87
98
|
// Written on first spawn so constructing the adapter touches no state.
|
|
88
99
|
private settingsFile: string | undefined;
|
|
@@ -92,44 +103,60 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
92
103
|
|
|
93
104
|
private readonly bridgeTarget: string | undefined;
|
|
94
105
|
|
|
95
|
-
constructor(
|
|
106
|
+
constructor(
|
|
107
|
+
entry: AgentEntry,
|
|
108
|
+
config: Pick<Config, 'authProfiles'>,
|
|
109
|
+
headlessRun: ClaudeHeadlessRun | null = null,
|
|
110
|
+
bridgeTarget?: string,
|
|
111
|
+
) {
|
|
96
112
|
this.bridgeTarget = bridgeTarget;
|
|
97
|
-
this.
|
|
113
|
+
this.entry = entry;
|
|
114
|
+
this.authProfiles = config.authProfiles;
|
|
115
|
+
this.id = entry.id;
|
|
116
|
+
this.hookAgent = entry.id;
|
|
98
117
|
|
|
99
118
|
this.profile = {
|
|
100
|
-
label:
|
|
119
|
+
label: entry.label,
|
|
101
120
|
kind: 'claude',
|
|
102
|
-
bin:
|
|
121
|
+
bin: entry.bin,
|
|
103
122
|
models: null,
|
|
104
|
-
spawnOptions: buildClaudeSpawnOptions(
|
|
123
|
+
spawnOptions: buildClaudeSpawnOptions(entry.args),
|
|
105
124
|
};
|
|
106
125
|
|
|
107
126
|
this.headlessRunner =
|
|
108
127
|
headlessRun === null
|
|
109
128
|
? null
|
|
110
129
|
: makeClaudeHeadlessRunner(headlessRun, {
|
|
111
|
-
claudeBin:
|
|
112
|
-
permissionMode: resolveClaudePermissionMode(
|
|
130
|
+
claudeBin: entry.bin,
|
|
131
|
+
permissionMode: resolveClaudePermissionMode(entry.args, entry.settings),
|
|
113
132
|
pluginDir: () => this.writeBridge(),
|
|
133
|
+
settings: () => this.writeSettings(),
|
|
114
134
|
});
|
|
115
135
|
}
|
|
116
136
|
|
|
117
137
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
|
118
|
-
|
|
138
|
+
const modeArgs =
|
|
139
|
+
opts.resume === false ? [] : buildRestoreModeArgs(this.entry.args, this.entry.settings);
|
|
119
140
|
|
|
120
141
|
return {
|
|
121
|
-
bin: this.
|
|
122
|
-
args: this.buildArgs(
|
|
142
|
+
bin: this.entry.bin,
|
|
143
|
+
args: this.buildArgs(
|
|
144
|
+
this.entry.args,
|
|
145
|
+
opts,
|
|
146
|
+
modeArgs,
|
|
147
|
+
this.writeSettings(),
|
|
148
|
+
this.writeBridge(),
|
|
149
|
+
),
|
|
123
150
|
};
|
|
124
151
|
}
|
|
125
152
|
|
|
126
|
-
// With `
|
|
153
|
+
// With `auth` configured, Claude signs in with the subscription
|
|
127
154
|
// token impd's broker holds on a target that reaches the broker, and
|
|
128
155
|
// with the sign-in of the host it runs on anywhere else.
|
|
129
156
|
findAuthSelection(): AuthSelection | null {
|
|
130
|
-
const auth = this.
|
|
157
|
+
const auth = this.entry.auth;
|
|
131
158
|
|
|
132
|
-
if (auth ===
|
|
159
|
+
if (auth === undefined) {
|
|
133
160
|
return null;
|
|
134
161
|
}
|
|
135
162
|
|
|
@@ -139,7 +166,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
139
166
|
baseURL: ANTHROPIC_API_URL,
|
|
140
167
|
auth: { profiles: auth.profiles, placeholderEnv: { [OAUTH_VARIABLE]: PLACEHOLDER } },
|
|
141
168
|
},
|
|
142
|
-
profiles: this.
|
|
169
|
+
profiles: this.authProfiles,
|
|
143
170
|
brokerRequired: false,
|
|
144
171
|
};
|
|
145
172
|
}
|
|
@@ -156,6 +183,9 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
156
183
|
|
|
157
184
|
const argv = [guest.atc];
|
|
158
185
|
|
|
186
|
+
const modeArgs =
|
|
187
|
+
opts.resume === false ? [] : buildRestoreModeArgs(this.entry.args, this.entry.settings);
|
|
188
|
+
|
|
159
189
|
const bridge = Object.entries(buildATCBridgeFiles(argv)).map(
|
|
160
190
|
([path, content]): [string, string] => [`atc-bridge/${path}`, content],
|
|
161
191
|
);
|
|
@@ -165,15 +195,20 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
165
195
|
}
|
|
166
196
|
|
|
167
197
|
return {
|
|
168
|
-
bin: this.
|
|
198
|
+
bin: this.entry.bin,
|
|
169
199
|
args: this.buildArgs(
|
|
170
|
-
this.
|
|
200
|
+
this.entry.args,
|
|
171
201
|
opts,
|
|
202
|
+
modeArgs,
|
|
172
203
|
`${guest.dir}/settings.json`,
|
|
173
204
|
`${guest.dir}/atc-bridge`,
|
|
174
205
|
),
|
|
175
206
|
files: {
|
|
176
|
-
'settings.json': JSON.stringify(
|
|
207
|
+
'settings.json': JSON.stringify(
|
|
208
|
+
buildHookSettings(this.buildSettingsProfile({}), 0, argv),
|
|
209
|
+
null,
|
|
210
|
+
2,
|
|
211
|
+
),
|
|
177
212
|
...Object.fromEntries(bridge),
|
|
178
213
|
},
|
|
179
214
|
};
|
|
@@ -183,7 +218,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
183
218
|
// broker with trust for the clone; any other remote session reads the
|
|
184
219
|
// config of the host's image, which atc never writes.
|
|
185
220
|
planGuestWorkspaceTrust(root: string): Readonly<Record<string, string>> | null {
|
|
186
|
-
if (this.
|
|
221
|
+
if (this.entry.auth === undefined) {
|
|
187
222
|
return null;
|
|
188
223
|
}
|
|
189
224
|
|
|
@@ -217,10 +252,11 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
217
252
|
const launch = buildClaudeGuestLaunch(
|
|
218
253
|
dir,
|
|
219
254
|
[
|
|
220
|
-
this.
|
|
255
|
+
this.entry.bin,
|
|
221
256
|
...this.buildArgs(
|
|
222
|
-
buildArgsWithoutFlags(this.
|
|
257
|
+
buildArgsWithoutFlags(this.entry.args, ['--permission-mode']),
|
|
223
258
|
opts,
|
|
259
|
+
[],
|
|
224
260
|
`${dir}/${settingsPath}`,
|
|
225
261
|
`${dir}/atc-bridge`,
|
|
226
262
|
),
|
|
@@ -238,7 +274,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
238
274
|
const padding = typeof userSettings === 'string' ? findStatuslinePadding(userSettings) : 0;
|
|
239
275
|
|
|
240
276
|
const settings = buildHookSettings(
|
|
241
|
-
|
|
277
|
+
this.buildSettingsProfile({ ...auth.env, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1' }),
|
|
242
278
|
padding,
|
|
243
279
|
argv,
|
|
244
280
|
);
|
|
@@ -261,50 +297,81 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
261
297
|
};
|
|
262
298
|
}
|
|
263
299
|
|
|
264
|
-
// The refusal for
|
|
265
|
-
//
|
|
266
|
-
// around impd's broker, or null
|
|
300
|
+
// The refusal for an entry whose environment, settings environment, or
|
|
301
|
+
// inline `--settings` argument sets a variable that overrides the
|
|
302
|
+
// subscription sign-in or routes the CLI around impd's broker, or null
|
|
303
|
+
// when it sets none.
|
|
267
304
|
private findCredentialOverride(): DaemonError | null {
|
|
268
|
-
const
|
|
305
|
+
const settingsEnv = this.entry.settings?.['env'];
|
|
269
306
|
|
|
270
|
-
|
|
271
|
-
|
|
307
|
+
const sources: readonly (readonly [string, readonly string[]])[] = [
|
|
308
|
+
['env', Object.keys(this.entry.env)],
|
|
309
|
+
['settings.env', isRecord(settingsEnv) ? Object.keys(settingsEnv) : []],
|
|
310
|
+
['args in --settings', this.findInlineSettingsKeys()],
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
for (const [source, keys] of sources) {
|
|
314
|
+
const variable = keys.find((key) => isSubscriptionOverrideVariable(key));
|
|
315
|
+
|
|
316
|
+
if (variable !== undefined) {
|
|
317
|
+
return new DaemonError(
|
|
318
|
+
'auth_target_unsupported',
|
|
319
|
+
`claude signs in through impd's broker on this target, but agents.${this.id}.${source === 'args in --settings' ? 'args set' : `${source} sets`} ${variable}${source === 'args in --settings' ? ' in --settings' : ''}, which would override or route around that sign-in`,
|
|
320
|
+
{ agent: this.id, problem: 'guest_env_conflict', variable },
|
|
321
|
+
);
|
|
322
|
+
}
|
|
272
323
|
}
|
|
273
324
|
|
|
274
|
-
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// The variables an inline `--settings` argument sets in its env block.
|
|
329
|
+
private findInlineSettingsKeys(): string[] {
|
|
330
|
+
const inline = findFlagValue(this.entry.args, ['--settings']);
|
|
331
|
+
|
|
332
|
+
if (inline === null) {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
275
335
|
|
|
276
336
|
try {
|
|
277
|
-
parsed = JSON.parse(inline);
|
|
337
|
+
const parsed: unknown = JSON.parse(inline);
|
|
338
|
+
const env = isRecord(parsed) ? parsed['env'] : undefined;
|
|
339
|
+
|
|
340
|
+
return isRecord(env) ? Object.keys(env) : [];
|
|
278
341
|
} catch {
|
|
279
|
-
return
|
|
342
|
+
return [];
|
|
280
343
|
}
|
|
344
|
+
}
|
|
281
345
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
);
|
|
346
|
+
// What the generated settings file holds for this entry: its hooks and
|
|
347
|
+
// statusline, its settings, and its environment, with `extraEnv` on top.
|
|
348
|
+
// The entry's environment outranks the settings' own env block.
|
|
349
|
+
private buildSettingsProfile(extraEnv: Readonly<Record<string, string>>): HookSettingsProfile {
|
|
350
|
+
const settingsEnv = this.entry.settings?.['env'];
|
|
288
351
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
352
|
+
const env = {
|
|
353
|
+
...(isRecord(settingsEnv) ? toStringEntries(settingsEnv) : {}),
|
|
354
|
+
...this.entry.env,
|
|
355
|
+
...extraEnv,
|
|
356
|
+
};
|
|
292
357
|
|
|
293
|
-
return
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
{
|
|
297
|
-
|
|
358
|
+
return {
|
|
359
|
+
id: this.id,
|
|
360
|
+
env,
|
|
361
|
+
...(this.entry.settings === undefined ? {} : { settings: this.entry.settings }),
|
|
362
|
+
};
|
|
298
363
|
}
|
|
299
364
|
|
|
300
365
|
private buildArgs(
|
|
301
366
|
configured: readonly string[],
|
|
302
367
|
opts: SpawnOptions,
|
|
368
|
+
modeArgs: readonly string[],
|
|
303
369
|
settings: string,
|
|
304
370
|
pluginDir: string,
|
|
305
371
|
): string[] {
|
|
306
372
|
return [
|
|
307
373
|
...buildClaudeOverrideArgs(configured, opts),
|
|
374
|
+
...modeArgs,
|
|
308
375
|
'--settings',
|
|
309
376
|
settings,
|
|
310
377
|
'--plugin-dir',
|
|
@@ -315,6 +382,12 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
315
382
|
];
|
|
316
383
|
}
|
|
317
384
|
|
|
385
|
+
private writeSettings(): string {
|
|
386
|
+
this.settingsFile ??= writeHookSettings(this.buildSettingsProfile({}));
|
|
387
|
+
|
|
388
|
+
return this.settingsFile;
|
|
389
|
+
}
|
|
390
|
+
|
|
318
391
|
private writeBridge(): string {
|
|
319
392
|
this.bridgeDir ??= writeATCBridge(this.bridgeTarget);
|
|
320
393
|
|
|
@@ -453,13 +526,21 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
453
526
|
|
|
454
527
|
// Shell command that re-opens this session outside atc (or anywhere). A
|
|
455
528
|
// permission mode the configured arguments set travels as an explicit
|
|
456
|
-
// flag, so it overrides the mode the CLI would restore.
|
|
529
|
+
// flag, so it overrides the mode the CLI would restore. The generated
|
|
530
|
+
// settings file travels only for an entry that sets its own settings or
|
|
531
|
+
// environment, since the file sits on the daemon's machine and an entry
|
|
532
|
+
// without either needs nothing from it.
|
|
457
533
|
buildResumeCommand(cwd: string, agentSessionID: AgentSessionID | undefined): string | null {
|
|
458
|
-
const configured = findClaudePermissionMode(this.
|
|
534
|
+
const configured = findClaudePermissionMode(this.entry.args, this.entry.settings);
|
|
459
535
|
const mode = configured === null ? '' : ` --permission-mode ${toShellArg(configured)}`;
|
|
460
536
|
const resume = agentSessionID === undefined ? '' : ` ${agentSessionID}`;
|
|
461
537
|
|
|
462
|
-
|
|
538
|
+
const settings =
|
|
539
|
+
this.entry.settings === undefined && Object.keys(this.entry.env).length === 0
|
|
540
|
+
? ''
|
|
541
|
+
: ` --settings ${toShellArg(this.writeSettings())}`;
|
|
542
|
+
|
|
543
|
+
return `cd ${toShellArg(cwd)} && ${formatShellWord(this.entry.bin)}${mode}${settings} --resume${resume}`;
|
|
463
544
|
}
|
|
464
545
|
}
|
|
465
546
|
|
|
@@ -470,21 +551,14 @@ const ANTHROPIC_API_URL = 'https://api.anthropic.com';
|
|
|
470
551
|
const OAUTH_VARIABLE = 'CLAUDE_CODE_OAUTH_TOKEN';
|
|
471
552
|
const PLACEHOLDER = 'imp-broker-placeholder';
|
|
472
553
|
|
|
473
|
-
// The
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
'CLAUDE_CODE_USE_VERTEX',
|
|
482
|
-
'CLAUDE_CODE_USE_FOUNDRY',
|
|
483
|
-
'CLAUDE_CODE_USE_MANTLE',
|
|
484
|
-
'CLAUDE_CODE_USE_ANTHROPIC_AWS',
|
|
485
|
-
'CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD',
|
|
486
|
-
'CLAUDE_CODE_USE_GATEWAY',
|
|
487
|
-
]);
|
|
554
|
+
// The string values of an object, which is all a session's environment holds.
|
|
555
|
+
function toStringEntries(value: Readonly<Record<string, unknown>>): Record<string, string> {
|
|
556
|
+
return Object.fromEntries(
|
|
557
|
+
Object.entries(value).filter(
|
|
558
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
559
|
+
),
|
|
560
|
+
);
|
|
561
|
+
}
|
|
488
562
|
|
|
489
563
|
// The padding of the statusline the bundle's user settings set, which atc's
|
|
490
564
|
// own statusline mirrors since it renders that one first, or none.
|
|
@@ -535,3 +609,9 @@ function buildClaudeSpawnOptions(claudeArgs: readonly string[]): SpawnOptionSpec
|
|
|
535
609
|
},
|
|
536
610
|
};
|
|
537
611
|
}
|
|
612
|
+
|
|
613
|
+
// A binary name or path that needs no quoting stays bare, so the default
|
|
614
|
+
// entry resumes with plain `claude`; anything else is quoted as one word.
|
|
615
|
+
function formatShellWord(value: string): string {
|
|
616
|
+
return /^[\w./-]+$/.test(value) ? value : toShellArg(value);
|
|
617
|
+
}
|
|
@@ -3,9 +3,10 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
5
5
|
import type { HookEvent } from '../protocol/hook-event';
|
|
6
|
+
import type { AgentID } from '../shared/agent-id';
|
|
6
7
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
7
8
|
import { buildOptionalString } from '../shared/build-optional-string';
|
|
8
|
-
import type {
|
|
9
|
+
import type { AgentEntry } from '../shared/collect-agents';
|
|
9
10
|
import { isRecord } from '../shared/report';
|
|
10
11
|
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
11
12
|
import { toShellArg } from '../shared/to-shell-arg';
|
|
@@ -47,7 +48,11 @@ const CODEX_MODEL_FLAGS = ['-m', '--model'];
|
|
|
47
48
|
* trusts once in the Codex TUI; atc never writes into the user's Codex config.
|
|
48
49
|
*/
|
|
49
50
|
export class CodexAdapter implements AgentAdapter {
|
|
50
|
-
readonly id
|
|
51
|
+
readonly id: AgentID;
|
|
52
|
+
|
|
53
|
+
// The hook files a user installs print the kind for every entry of it, so
|
|
54
|
+
// the hook lines of every codex entry carry it.
|
|
55
|
+
readonly hookAgent = 'codex';
|
|
51
56
|
|
|
52
57
|
// Codex has no headless handoff.
|
|
53
58
|
readonly headlessRunner = null;
|
|
@@ -63,29 +68,30 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
63
68
|
// line is pasted and then submitted.
|
|
64
69
|
readonly planLineInput = planPastedLineInput;
|
|
65
70
|
|
|
66
|
-
private readonly
|
|
71
|
+
private readonly entry: AgentEntry;
|
|
67
72
|
|
|
68
|
-
constructor(
|
|
69
|
-
this.
|
|
73
|
+
constructor(entry: AgentEntry) {
|
|
74
|
+
this.entry = entry;
|
|
75
|
+
this.id = entry.id;
|
|
70
76
|
|
|
71
77
|
this.profile = {
|
|
72
|
-
label:
|
|
78
|
+
label: entry.label,
|
|
73
79
|
kind: 'codex',
|
|
74
|
-
bin:
|
|
80
|
+
bin: entry.bin,
|
|
75
81
|
models: null,
|
|
76
|
-
spawnOptions: buildCodexSpawnOptions(
|
|
82
|
+
spawnOptions: buildCodexSpawnOptions(entry.args),
|
|
77
83
|
};
|
|
78
84
|
}
|
|
79
85
|
|
|
80
86
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
|
81
87
|
return {
|
|
82
|
-
bin: this.
|
|
88
|
+
bin: this.entry.bin,
|
|
83
89
|
args: [
|
|
84
90
|
// A model override replaces any model flag the configured arguments
|
|
85
91
|
// carry, and travels as its own argument.
|
|
86
92
|
...(opts.model === undefined
|
|
87
|
-
? this.
|
|
88
|
-
: [...buildArgsWithoutFlags(this.
|
|
93
|
+
? this.entry.args
|
|
94
|
+
: [...buildArgsWithoutFlags(this.entry.args, CODEX_MODEL_FLAGS), '-m', opts.model]),
|
|
89
95
|
|
|
90
96
|
// Bare resume opens Codex's own session picker; an id resumes that
|
|
91
97
|
// session directly. Both accept a prompt afterwards.
|
|
@@ -50,6 +50,9 @@ import { writeHookSettings } from './write-hook-settings';
|
|
|
50
50
|
export class GatewayAdapter implements AgentAdapter {
|
|
51
51
|
readonly id: AgentID;
|
|
52
52
|
|
|
53
|
+
// The settings file names the gateway's id on every hook command.
|
|
54
|
+
readonly hookAgent: AgentID;
|
|
55
|
+
|
|
53
56
|
// A headless turn carries the same settings file the terminal spawn does,
|
|
54
57
|
// so it reaches this backend rather than the default one. A gateway whose
|
|
55
58
|
// credential comes through impd's broker runs no headless turn.
|
|
@@ -103,7 +106,20 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
103
106
|
spawnOptions: buildGatewaySpawnOptions(gateway.args, models),
|
|
104
107
|
};
|
|
105
108
|
|
|
106
|
-
this.
|
|
109
|
+
this.hookAgent = gateway.id;
|
|
110
|
+
|
|
111
|
+
this.claude = new ClaudeAdapter(
|
|
112
|
+
{
|
|
113
|
+
id: gateway.id,
|
|
114
|
+
kind: 'claude',
|
|
115
|
+
label: gateway.label,
|
|
116
|
+
mark: gateway.mark,
|
|
117
|
+
bin: gateway.bin,
|
|
118
|
+
args: gateway.args,
|
|
119
|
+
env: {},
|
|
120
|
+
},
|
|
121
|
+
config,
|
|
122
|
+
);
|
|
107
123
|
|
|
108
124
|
this.headlessRunner =
|
|
109
125
|
headlessRun === null || gateway.auth !== undefined
|
|
@@ -3,9 +3,10 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
5
5
|
import type { HookEvent } from '../protocol/hook-event';
|
|
6
|
+
import type { AgentID } from '../shared/agent-id';
|
|
6
7
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
7
8
|
import { buildOptionalString } from '../shared/build-optional-string';
|
|
8
|
-
import type {
|
|
9
|
+
import type { AgentEntry } from '../shared/collect-agents';
|
|
9
10
|
import { isRecord } from '../shared/report';
|
|
10
11
|
import type { SessionID } from '../shared/session-id';
|
|
11
12
|
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
@@ -65,7 +66,11 @@ const GROK_UNSUPPORTED_OPTION: SpawnOptionSpec = {
|
|
|
65
66
|
* resume semantics, and summary.json name-pulling.
|
|
66
67
|
*/
|
|
67
68
|
export class GrokAdapter implements AgentAdapter {
|
|
68
|
-
readonly id
|
|
69
|
+
readonly id: AgentID;
|
|
70
|
+
|
|
71
|
+
// The hook files a user installs print the kind for every entry of it, so
|
|
72
|
+
// the hook lines of every grok entry carry it.
|
|
73
|
+
readonly hookAgent = 'grok';
|
|
69
74
|
|
|
70
75
|
// Grok has no headless handoff.
|
|
71
76
|
readonly headlessRunner = null;
|
|
@@ -81,17 +86,18 @@ export class GrokAdapter implements AgentAdapter {
|
|
|
81
86
|
// line is pasted and then submitted.
|
|
82
87
|
readonly planLineInput = planPastedLineInput;
|
|
83
88
|
|
|
84
|
-
private readonly
|
|
89
|
+
private readonly entry: AgentEntry;
|
|
85
90
|
|
|
86
91
|
private readonly hookState = new Map<SessionID, GrokSessionHookState>();
|
|
87
92
|
|
|
88
|
-
constructor(
|
|
89
|
-
this.
|
|
93
|
+
constructor(entry: AgentEntry) {
|
|
94
|
+
this.entry = entry;
|
|
95
|
+
this.id = entry.id;
|
|
90
96
|
|
|
91
97
|
this.profile = {
|
|
92
|
-
label:
|
|
98
|
+
label: entry.label,
|
|
93
99
|
kind: 'grok',
|
|
94
|
-
bin:
|
|
100
|
+
bin: entry.bin,
|
|
95
101
|
models: null,
|
|
96
102
|
spawnOptions: { model: GROK_UNSUPPORTED_OPTION, effort: GROK_UNSUPPORTED_OPTION },
|
|
97
103
|
};
|
|
@@ -99,9 +105,9 @@ export class GrokAdapter implements AgentAdapter {
|
|
|
99
105
|
|
|
100
106
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
|
101
107
|
return {
|
|
102
|
-
bin: this.
|
|
108
|
+
bin: this.entry.bin,
|
|
103
109
|
args: [
|
|
104
|
-
...this.
|
|
110
|
+
...this.entry.args.filter((arg) => arg !== '--leader' && arg !== '--no-leader'),
|
|
105
111
|
'--no-leader',
|
|
106
112
|
...(typeof opts.resume === 'string' ? ['--resume', opts.resume] : []),
|
|
107
113
|
...(opts.prompt === '' ? [] : [opts.prompt]),
|