@zgeoff/atc 2.36.1 → 2.38.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/build-claude-config-seed.ts +7 -4
- package/src/agents/build-claude-mcp-config.ts +27 -0
- package/src/agents/claude-adapter.ts +157 -66
- 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 +196 -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 +349 -0
- package/src/shared/collect-claude-auth.ts +125 -10
- package/src/shared/collect-gateways.ts +2 -168
- package/src/shared/collect-legacy-agents.ts +80 -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,12 @@ import { CLAUDE_CONFIG_SEED_FILE } from './claude-config-seed-file';
|
|
|
4
4
|
* The guest file that seeds a session's own Claude config folder with the
|
|
5
5
|
* state the CLI reads from `.claude.json` to start without its first-run
|
|
6
6
|
* onboarding, and with folder trust for `trustedRoot`, the exact root of a
|
|
7
|
-
* verified clone, when one is given.
|
|
7
|
+
* verified clone, when one is given. Trust also approves every MCP server
|
|
8
|
+
* the clone's own `.mcp.json` declares, so an unattended session never
|
|
9
|
+
* waits on the CLI's approval prompt. It holds no account: the placeholder
|
|
8
10
|
* credential is what keeps the CLI from asking for a login. Without a
|
|
9
|
-
* trusted root the CLI asks a person to trust the workspace
|
|
10
|
-
* start.
|
|
11
|
+
* trusted root the CLI asks a person to trust the workspace, and to approve
|
|
12
|
+
* the clone's MCP servers, on its first start.
|
|
11
13
|
*/
|
|
12
14
|
export function buildClaudeConfigSeed(
|
|
13
15
|
trustedRoot: string | null,
|
|
@@ -15,9 +17,10 @@ export function buildClaudeConfigSeed(
|
|
|
15
17
|
const seed =
|
|
16
18
|
trustedRoot === null
|
|
17
19
|
? ONBOARDED_CONFIG
|
|
18
|
-
: { ...ONBOARDED_CONFIG, projects: { [trustedRoot]:
|
|
20
|
+
: { ...ONBOARDED_CONFIG, projects: { [trustedRoot]: TRUSTED_PROJECT } };
|
|
19
21
|
|
|
20
22
|
return { [CLAUDE_CONFIG_SEED_FILE]: JSON.stringify(seed, null, 2) };
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const ONBOARDED_CONFIG = { hasCompletedOnboarding: true };
|
|
26
|
+
const TRUSTED_PROJECT = { hasTrustDialogAccepted: true, enableAllProjectMcpServers: true };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ClaudeMCPServer } from '../shared/collect-claude-auth';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The MCP config file that gives a session each server over HTTP with the
|
|
5
|
+
* broker's placeholder as a bearer credential in the server's header, so
|
|
6
|
+
* impd's broker swaps in the credential it holds for the server's host and
|
|
7
|
+
* the session never holds it.
|
|
8
|
+
*/
|
|
9
|
+
export function buildClaudeMCPConfig(servers: readonly ClaudeMCPServer[]): {
|
|
10
|
+
readonly mcpServers: Readonly<Record<string, unknown>>;
|
|
11
|
+
} {
|
|
12
|
+
return {
|
|
13
|
+
mcpServers: Object.fromEntries(
|
|
14
|
+
servers.map((server) => [
|
|
15
|
+
server.name,
|
|
16
|
+
{
|
|
17
|
+
type: 'http',
|
|
18
|
+
url: server.url,
|
|
19
|
+
headers: { [server.header]: `Bearer ${BROKER_PLACEHOLDER}` },
|
|
20
|
+
},
|
|
21
|
+
]),
|
|
22
|
+
),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The value impd's broker replaces with the credential on the host's side.
|
|
27
|
+
const BROKER_PLACEHOLDER = 'imp-broker-placeholder';
|
|
@@ -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';
|
|
@@ -29,8 +32,11 @@ import { buildArgsWithoutFlags } from './build-args-without-flags';
|
|
|
29
32
|
import { buildATCBridgeFiles } from './build-atc-bridge-files';
|
|
30
33
|
import { buildClaudeConfigSeed } from './build-claude-config-seed';
|
|
31
34
|
import { buildClaudeGuestLaunch } from './build-claude-guest-launch';
|
|
35
|
+
import { buildClaudeMCPConfig } from './build-claude-mcp-config';
|
|
32
36
|
import { buildClaudeOverrideArgs } from './build-claude-override-args';
|
|
33
37
|
import { buildHookSettings } from './build-hook-settings';
|
|
38
|
+
import type { HookSettingsProfile } from './build-hook-settings';
|
|
39
|
+
import { buildRestoreModeArgs } from './build-restore-mode-args';
|
|
34
40
|
import { CLAUDE_CONFIG_BUNDLE_FOLDER } from './claude-config-bundle-folder';
|
|
35
41
|
import { CLAUDE_EFFORT_LEVELS } from './claude-effort-levels';
|
|
36
42
|
import { findClaudePermissionMode } from './find-claude-permission-mode';
|
|
@@ -65,7 +71,11 @@ type ClaudeHookPayload = z.infer<typeof CLAUDE_HOOK_PAYLOAD_SCHEMA>;
|
|
|
65
71
|
* resume semantics, transcript name-pulling, and statusline chaining.
|
|
66
72
|
*/
|
|
67
73
|
export class ClaudeAdapter implements AgentAdapter {
|
|
68
|
-
readonly id
|
|
74
|
+
readonly id: AgentID;
|
|
75
|
+
|
|
76
|
+
// The agent value the hook lines of a Claude session carry: the settings
|
|
77
|
+
// file atc writes for this id names it on each hook command.
|
|
78
|
+
readonly hookAgent: AgentID;
|
|
69
79
|
|
|
70
80
|
readonly headlessRunner: HeadlessRunner | null;
|
|
71
81
|
|
|
@@ -82,7 +92,9 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
82
92
|
|
|
83
93
|
readonly takesMessages = true;
|
|
84
94
|
|
|
85
|
-
private readonly
|
|
95
|
+
private readonly entry: AgentEntry;
|
|
96
|
+
|
|
97
|
+
private readonly authProfiles: Config['authProfiles'];
|
|
86
98
|
|
|
87
99
|
// Written on first spawn so constructing the adapter touches no state.
|
|
88
100
|
private settingsFile: string | undefined;
|
|
@@ -92,44 +104,60 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
92
104
|
|
|
93
105
|
private readonly bridgeTarget: string | undefined;
|
|
94
106
|
|
|
95
|
-
constructor(
|
|
107
|
+
constructor(
|
|
108
|
+
entry: AgentEntry,
|
|
109
|
+
config: Pick<Config, 'authProfiles'>,
|
|
110
|
+
headlessRun: ClaudeHeadlessRun | null = null,
|
|
111
|
+
bridgeTarget?: string,
|
|
112
|
+
) {
|
|
96
113
|
this.bridgeTarget = bridgeTarget;
|
|
97
|
-
this.
|
|
114
|
+
this.entry = entry;
|
|
115
|
+
this.authProfiles = config.authProfiles;
|
|
116
|
+
this.id = entry.id;
|
|
117
|
+
this.hookAgent = entry.id;
|
|
98
118
|
|
|
99
119
|
this.profile = {
|
|
100
|
-
label:
|
|
120
|
+
label: entry.label,
|
|
101
121
|
kind: 'claude',
|
|
102
|
-
bin:
|
|
122
|
+
bin: entry.bin,
|
|
103
123
|
models: null,
|
|
104
|
-
spawnOptions: buildClaudeSpawnOptions(
|
|
124
|
+
spawnOptions: buildClaudeSpawnOptions(entry.args),
|
|
105
125
|
};
|
|
106
126
|
|
|
107
127
|
this.headlessRunner =
|
|
108
128
|
headlessRun === null
|
|
109
129
|
? null
|
|
110
130
|
: makeClaudeHeadlessRunner(headlessRun, {
|
|
111
|
-
claudeBin:
|
|
112
|
-
permissionMode: resolveClaudePermissionMode(
|
|
131
|
+
claudeBin: entry.bin,
|
|
132
|
+
permissionMode: resolveClaudePermissionMode(entry.args, entry.settings),
|
|
113
133
|
pluginDir: () => this.writeBridge(),
|
|
134
|
+
settings: () => this.writeSettings(),
|
|
114
135
|
});
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
|
118
|
-
|
|
139
|
+
const modeArgs =
|
|
140
|
+
opts.resume === false ? [] : buildRestoreModeArgs(this.entry.args, this.entry.settings);
|
|
119
141
|
|
|
120
142
|
return {
|
|
121
|
-
bin: this.
|
|
122
|
-
args: this.buildArgs(
|
|
143
|
+
bin: this.entry.bin,
|
|
144
|
+
args: this.buildArgs(
|
|
145
|
+
this.entry.args,
|
|
146
|
+
opts,
|
|
147
|
+
modeArgs,
|
|
148
|
+
this.writeSettings(),
|
|
149
|
+
this.writeBridge(),
|
|
150
|
+
),
|
|
123
151
|
};
|
|
124
152
|
}
|
|
125
153
|
|
|
126
|
-
// With `
|
|
154
|
+
// With `auth` configured, Claude signs in with the subscription
|
|
127
155
|
// token impd's broker holds on a target that reaches the broker, and
|
|
128
156
|
// with the sign-in of the host it runs on anywhere else.
|
|
129
157
|
findAuthSelection(): AuthSelection | null {
|
|
130
|
-
const auth = this.
|
|
158
|
+
const auth = this.entry.auth;
|
|
131
159
|
|
|
132
|
-
if (auth ===
|
|
160
|
+
if (auth === undefined) {
|
|
133
161
|
return null;
|
|
134
162
|
}
|
|
135
163
|
|
|
@@ -139,7 +167,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
139
167
|
baseURL: ANTHROPIC_API_URL,
|
|
140
168
|
auth: { profiles: auth.profiles, placeholderEnv: { [OAUTH_VARIABLE]: PLACEHOLDER } },
|
|
141
169
|
},
|
|
142
|
-
profiles: this.
|
|
170
|
+
profiles: this.authProfiles,
|
|
143
171
|
brokerRequired: false,
|
|
144
172
|
};
|
|
145
173
|
}
|
|
@@ -156,6 +184,9 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
156
184
|
|
|
157
185
|
const argv = [guest.atc];
|
|
158
186
|
|
|
187
|
+
const modeArgs =
|
|
188
|
+
opts.resume === false ? [] : buildRestoreModeArgs(this.entry.args, this.entry.settings);
|
|
189
|
+
|
|
159
190
|
const bridge = Object.entries(buildATCBridgeFiles(argv)).map(
|
|
160
191
|
([path, content]): [string, string] => [`atc-bridge/${path}`, content],
|
|
161
192
|
);
|
|
@@ -165,15 +196,20 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
165
196
|
}
|
|
166
197
|
|
|
167
198
|
return {
|
|
168
|
-
bin: this.
|
|
199
|
+
bin: this.entry.bin,
|
|
169
200
|
args: this.buildArgs(
|
|
170
|
-
this.
|
|
201
|
+
this.entry.args,
|
|
171
202
|
opts,
|
|
203
|
+
modeArgs,
|
|
172
204
|
`${guest.dir}/settings.json`,
|
|
173
205
|
`${guest.dir}/atc-bridge`,
|
|
174
206
|
),
|
|
175
207
|
files: {
|
|
176
|
-
'settings.json': JSON.stringify(
|
|
208
|
+
'settings.json': JSON.stringify(
|
|
209
|
+
buildHookSettings(this.buildSettingsProfile({}), 0, argv),
|
|
210
|
+
null,
|
|
211
|
+
2,
|
|
212
|
+
),
|
|
177
213
|
...Object.fromEntries(bridge),
|
|
178
214
|
},
|
|
179
215
|
};
|
|
@@ -183,7 +219,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
183
219
|
// broker with trust for the clone; any other remote session reads the
|
|
184
220
|
// config of the host's image, which atc never writes.
|
|
185
221
|
planGuestWorkspaceTrust(root: string): Readonly<Record<string, string>> | null {
|
|
186
|
-
if (this.
|
|
222
|
+
if (this.entry.auth === undefined) {
|
|
187
223
|
return null;
|
|
188
224
|
}
|
|
189
225
|
|
|
@@ -197,7 +233,11 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
197
233
|
// config bundle, come from the host's own Claude config folder as it is
|
|
198
234
|
// at this launch, staged under a key of this launch's own. A credential, endpoint, or provider variable in the
|
|
199
235
|
// configured settings, or in the host's environment, would keep the CLI
|
|
200
|
-
// from sending the placeholder, so either refuses the start.
|
|
236
|
+
// from sending the placeholder, so either refuses the start. The entry's
|
|
237
|
+
// MCP servers reach the CLI through an MCP config file of the binding
|
|
238
|
+
// revision, each with the placeholder in its header. Their flag sits
|
|
239
|
+
// ahead of the settings flag, so the variadic flag never takes the
|
|
240
|
+
// prompt as one of its values.
|
|
201
241
|
private planSubscriptionGuestSpawn(
|
|
202
242
|
opts: SpawnOptions,
|
|
203
243
|
dir: string,
|
|
@@ -212,15 +252,19 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
212
252
|
}
|
|
213
253
|
|
|
214
254
|
const settingsPath = `auth-r${auth.revision}/settings.json`;
|
|
255
|
+
const mcpServers = this.entry.mcpServers ?? [];
|
|
256
|
+
const mcpPath = `auth-r${auth.revision}/mcp.json`;
|
|
257
|
+
const mcpArgs = mcpServers.length === 0 ? [] : ['--mcp-config', `${dir}/${mcpPath}`];
|
|
215
258
|
const bundleKey = randomUUID();
|
|
216
259
|
|
|
217
260
|
const launch = buildClaudeGuestLaunch(
|
|
218
261
|
dir,
|
|
219
262
|
[
|
|
220
|
-
this.
|
|
263
|
+
this.entry.bin,
|
|
221
264
|
...this.buildArgs(
|
|
222
|
-
buildArgsWithoutFlags(this.
|
|
265
|
+
buildArgsWithoutFlags(this.entry.args, ['--permission-mode']),
|
|
223
266
|
opts,
|
|
267
|
+
mcpArgs,
|
|
224
268
|
`${dir}/${settingsPath}`,
|
|
225
269
|
`${dir}/atc-bridge`,
|
|
226
270
|
),
|
|
@@ -238,7 +282,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
238
282
|
const padding = typeof userSettings === 'string' ? findStatuslinePadding(userSettings) : 0;
|
|
239
283
|
|
|
240
284
|
const settings = buildHookSettings(
|
|
241
|
-
|
|
285
|
+
this.buildSettingsProfile({ ...auth.env, CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1' }),
|
|
242
286
|
padding,
|
|
243
287
|
argv,
|
|
244
288
|
);
|
|
@@ -248,6 +292,9 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
248
292
|
args: launch.args,
|
|
249
293
|
files: {
|
|
250
294
|
[settingsPath]: JSON.stringify(settings, null, 2),
|
|
295
|
+
...(mcpServers.length === 0
|
|
296
|
+
? {}
|
|
297
|
+
: { [mcpPath]: JSON.stringify(buildClaudeMCPConfig(mcpServers), null, 2) }),
|
|
251
298
|
...buildClaudeConfigSeed(null),
|
|
252
299
|
...Object.fromEntries(
|
|
253
300
|
Object.entries(bundle).map(([path, content]) => [
|
|
@@ -261,50 +308,81 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
261
308
|
};
|
|
262
309
|
}
|
|
263
310
|
|
|
264
|
-
// The refusal for
|
|
265
|
-
//
|
|
266
|
-
// around impd's broker, or null
|
|
311
|
+
// The refusal for an entry whose environment, settings environment, or
|
|
312
|
+
// inline `--settings` argument sets a variable that overrides the
|
|
313
|
+
// subscription sign-in or routes the CLI around impd's broker, or null
|
|
314
|
+
// when it sets none.
|
|
267
315
|
private findCredentialOverride(): DaemonError | null {
|
|
268
|
-
const
|
|
316
|
+
const settingsEnv = this.entry.settings?.['env'];
|
|
269
317
|
|
|
270
|
-
|
|
271
|
-
|
|
318
|
+
const sources: readonly (readonly [string, readonly string[]])[] = [
|
|
319
|
+
['env', Object.keys(this.entry.env)],
|
|
320
|
+
['settings.env', isRecord(settingsEnv) ? Object.keys(settingsEnv) : []],
|
|
321
|
+
['args in --settings', this.findInlineSettingsKeys()],
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
for (const [source, keys] of sources) {
|
|
325
|
+
const variable = keys.find((key) => isSubscriptionOverrideVariable(key));
|
|
326
|
+
|
|
327
|
+
if (variable !== undefined) {
|
|
328
|
+
return new DaemonError(
|
|
329
|
+
'auth_target_unsupported',
|
|
330
|
+
`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`,
|
|
331
|
+
{ agent: this.id, problem: 'guest_env_conflict', variable },
|
|
332
|
+
);
|
|
333
|
+
}
|
|
272
334
|
}
|
|
273
335
|
|
|
274
|
-
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// The variables an inline `--settings` argument sets in its env block.
|
|
340
|
+
private findInlineSettingsKeys(): string[] {
|
|
341
|
+
const inline = findFlagValue(this.entry.args, ['--settings']);
|
|
342
|
+
|
|
343
|
+
if (inline === null) {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
275
346
|
|
|
276
347
|
try {
|
|
277
|
-
parsed = JSON.parse(inline);
|
|
348
|
+
const parsed: unknown = JSON.parse(inline);
|
|
349
|
+
const env = isRecord(parsed) ? parsed['env'] : undefined;
|
|
350
|
+
|
|
351
|
+
return isRecord(env) ? Object.keys(env) : [];
|
|
278
352
|
} catch {
|
|
279
|
-
return
|
|
353
|
+
return [];
|
|
280
354
|
}
|
|
355
|
+
}
|
|
281
356
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
);
|
|
357
|
+
// What the generated settings file holds for this entry: its hooks and
|
|
358
|
+
// statusline, its settings, and its environment, with `extraEnv` on top.
|
|
359
|
+
// The entry's environment outranks the settings' own env block.
|
|
360
|
+
private buildSettingsProfile(extraEnv: Readonly<Record<string, string>>): HookSettingsProfile {
|
|
361
|
+
const settingsEnv = this.entry.settings?.['env'];
|
|
288
362
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
363
|
+
const env = {
|
|
364
|
+
...(isRecord(settingsEnv) ? toStringEntries(settingsEnv) : {}),
|
|
365
|
+
...this.entry.env,
|
|
366
|
+
...extraEnv,
|
|
367
|
+
};
|
|
292
368
|
|
|
293
|
-
return
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
{
|
|
297
|
-
|
|
369
|
+
return {
|
|
370
|
+
id: this.id,
|
|
371
|
+
env,
|
|
372
|
+
...(this.entry.settings === undefined ? {} : { settings: this.entry.settings }),
|
|
373
|
+
};
|
|
298
374
|
}
|
|
299
375
|
|
|
300
376
|
private buildArgs(
|
|
301
377
|
configured: readonly string[],
|
|
302
378
|
opts: SpawnOptions,
|
|
379
|
+
leadArgs: readonly string[],
|
|
303
380
|
settings: string,
|
|
304
381
|
pluginDir: string,
|
|
305
382
|
): string[] {
|
|
306
383
|
return [
|
|
307
384
|
...buildClaudeOverrideArgs(configured, opts),
|
|
385
|
+
...leadArgs,
|
|
308
386
|
'--settings',
|
|
309
387
|
settings,
|
|
310
388
|
'--plugin-dir',
|
|
@@ -315,6 +393,12 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
315
393
|
];
|
|
316
394
|
}
|
|
317
395
|
|
|
396
|
+
private writeSettings(): string {
|
|
397
|
+
this.settingsFile ??= writeHookSettings(this.buildSettingsProfile({}));
|
|
398
|
+
|
|
399
|
+
return this.settingsFile;
|
|
400
|
+
}
|
|
401
|
+
|
|
318
402
|
private writeBridge(): string {
|
|
319
403
|
this.bridgeDir ??= writeATCBridge(this.bridgeTarget);
|
|
320
404
|
|
|
@@ -453,13 +537,21 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
453
537
|
|
|
454
538
|
// Shell command that re-opens this session outside atc (or anywhere). A
|
|
455
539
|
// permission mode the configured arguments set travels as an explicit
|
|
456
|
-
// flag, so it overrides the mode the CLI would restore.
|
|
540
|
+
// flag, so it overrides the mode the CLI would restore. The generated
|
|
541
|
+
// settings file travels only for an entry that sets its own settings or
|
|
542
|
+
// environment, since the file sits on the daemon's machine and an entry
|
|
543
|
+
// without either needs nothing from it.
|
|
457
544
|
buildResumeCommand(cwd: string, agentSessionID: AgentSessionID | undefined): string | null {
|
|
458
|
-
const configured = findClaudePermissionMode(this.
|
|
545
|
+
const configured = findClaudePermissionMode(this.entry.args, this.entry.settings);
|
|
459
546
|
const mode = configured === null ? '' : ` --permission-mode ${toShellArg(configured)}`;
|
|
460
547
|
const resume = agentSessionID === undefined ? '' : ` ${agentSessionID}`;
|
|
461
548
|
|
|
462
|
-
|
|
549
|
+
const settings =
|
|
550
|
+
this.entry.settings === undefined && Object.keys(this.entry.env).length === 0
|
|
551
|
+
? ''
|
|
552
|
+
: ` --settings ${toShellArg(this.writeSettings())}`;
|
|
553
|
+
|
|
554
|
+
return `cd ${toShellArg(cwd)} && ${formatShellWord(this.entry.bin)}${mode}${settings} --resume${resume}`;
|
|
463
555
|
}
|
|
464
556
|
}
|
|
465
557
|
|
|
@@ -470,21 +562,14 @@ const ANTHROPIC_API_URL = 'https://api.anthropic.com';
|
|
|
470
562
|
const OAUTH_VARIABLE = 'CLAUDE_CODE_OAUTH_TOKEN';
|
|
471
563
|
const PLACEHOLDER = 'imp-broker-placeholder';
|
|
472
564
|
|
|
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
|
-
]);
|
|
565
|
+
// The string values of an object, which is all a session's environment holds.
|
|
566
|
+
function toStringEntries(value: Readonly<Record<string, unknown>>): Record<string, string> {
|
|
567
|
+
return Object.fromEntries(
|
|
568
|
+
Object.entries(value).filter(
|
|
569
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
570
|
+
),
|
|
571
|
+
);
|
|
572
|
+
}
|
|
488
573
|
|
|
489
574
|
// The padding of the statusline the bundle's user settings set, which atc's
|
|
490
575
|
// own statusline mirrors since it renders that one first, or none.
|
|
@@ -535,3 +620,9 @@ function buildClaudeSpawnOptions(claudeArgs: readonly string[]): SpawnOptionSpec
|
|
|
535
620
|
},
|
|
536
621
|
};
|
|
537
622
|
}
|
|
623
|
+
|
|
624
|
+
// A binary name or path that needs no quoting stays bare, so the default
|
|
625
|
+
// entry resumes with plain `claude`; anything else is quoted as one word.
|
|
626
|
+
function formatShellWord(value: string): string {
|
|
627
|
+
return /^[\w./-]+$/.test(value) ? value : toShellArg(value);
|
|
628
|
+
}
|
|
@@ -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
|