@zgeoff/atc 2.11.0 → 2.12.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 +51 -0
- package/src/agents/build-args-without-flags.ts +25 -0
- package/src/agents/build-claude-override-args.ts +26 -0
- package/src/agents/claude-adapter.ts +49 -2
- package/src/agents/claude-effort-levels.ts +5 -0
- package/src/agents/codex-adapter.ts +43 -2
- package/src/agents/find-flag-value.ts +27 -0
- package/src/agents/gateway-adapter.ts +45 -2
- package/src/agents/grok-adapter.ts +19 -1
- package/src/cli.ts +18 -0
- package/src/client/daemon-client.ts +14 -2
- package/src/client/spawn-picker.ts +27 -4
- package/src/daemon/build-agent-list.ts +37 -1
- package/src/daemon/build-config-revision.ts +27 -0
- package/src/daemon/build-execution-targets.ts +34 -0
- package/src/daemon/build-fleet-events.ts +2 -1
- package/src/daemon/build-payload-hash.ts +31 -0
- package/src/daemon/build-scoped-context.ts +232 -0
- package/src/daemon/build-target-access.ts +33 -0
- package/src/daemon/build-target-forbidden-error.ts +13 -0
- package/src/daemon/build-target-identity.ts +22 -0
- package/src/daemon/build-target-list.ts +50 -0
- package/src/daemon/daemon-connection.ts +331 -95
- package/src/daemon/daemon.ts +452 -75
- package/src/daemon/effect-remains-error.ts +13 -0
- package/src/daemon/execution-provider.ts +103 -0
- package/src/daemon/find-execution-refusal.ts +104 -0
- package/src/daemon/idempotency-ledger.ts +164 -0
- package/src/daemon/local-pty-provider.ts +83 -0
- package/src/daemon/mint-session-id.ts +5 -5
- package/src/daemon/parse-spawn-overrides.ts +94 -0
- package/src/daemon/permission-registry.ts +14 -4
- package/src/daemon/restore-fleet.ts +66 -19
- package/src/daemon/session-runtime.ts +10 -0
- package/src/daemon/sessions.ts +282 -65
- package/src/daemon/start-headless-run.ts +11 -0
- package/src/daemon/start-headless-turn.ts +11 -2
- package/src/daemon/target-access.ts +36 -0
- package/src/mcp/answer-mcp-request.ts +12 -4
- package/src/mcp/answer-rpc-request.ts +28 -1
- package/src/mcp/build-principal-caller.ts +15 -0
- package/src/mcp/build-spawn-descriptions.ts +45 -0
- package/src/mcp/build-tool-list.ts +32 -4
- package/src/mcp/mcp-tools.ts +98 -8
- package/src/mcp/parse-idempotency-key.ts +29 -0
- package/src/mcp/reconnecting-caller.ts +39 -7
- package/src/mcp/require-daemon-features.ts +7 -0
- package/src/mcp/run-tool.ts +63 -11
- package/src/mcp/types.ts +2 -0
- package/src/protocol/daemon-error.ts +5 -1
- package/src/protocol/daemon-features.ts +24 -0
- package/src/protocol/protocol.ts +44 -11
- package/src/protocol/request-param-schemas.ts +60 -10
- package/src/shared/agent-session-id.ts +1 -1
- package/src/shared/collect-principals.ts +51 -0
- package/src/shared/collect-targets.ts +144 -0
- package/src/shared/config.ts +139 -14
- package/src/shared/daemon-id.ts +8 -0
- package/src/shared/format-json-kind.ts +21 -0
- package/src/shared/sort-json-keys.ts +19 -0
- package/src/shared/to-daemon-id.ts +11 -0
- package/src/store/fleet-entry.ts +42 -9
- package/src/store/idempotency-record.ts +48 -0
- package/src/store/message-owner.ts +1 -1
- package/src/store/run-migrations.ts +254 -5
- package/src/store/state-store.ts +433 -31
- package/src/workspace/check-workspace-completeness.ts +90 -0
- package/src/workspace/create-workspace-clone.ts +239 -0
- package/src/workspace/normalize-git-url.ts +59 -0
- package/src/workspace/read-workspace-tar.ts +38 -0
- package/src/workspace/resolve-path-source.ts +170 -0
- package/src/workspace/run-git.ts +103 -0
- package/src/workspace/sanitize-workspace-clone.ts +146 -0
- package/src/workspace/workspace-provenance.ts +11 -0
- package/src/workspace/workspace-source.ts +8 -0
package/package.json
CHANGED
|
@@ -25,6 +25,20 @@ export interface SpawnOptions {
|
|
|
25
25
|
// true opens the agent's own session picker; an agent session id resumes
|
|
26
26
|
// that specific session.
|
|
27
27
|
readonly resume: boolean | AgentSessionID;
|
|
28
|
+
|
|
29
|
+
// The model and effort the session runs with; an absent one leaves the
|
|
30
|
+
// agent's configured default in place.
|
|
31
|
+
readonly model?: string;
|
|
32
|
+
readonly effort?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The per-session model and effort a spawn asks for, already checked
|
|
37
|
+
* against what the agent advertises.
|
|
38
|
+
*/
|
|
39
|
+
export interface SpawnOverrides {
|
|
40
|
+
readonly model?: string;
|
|
41
|
+
readonly effort?: string;
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
export interface SpawnPlan {
|
|
@@ -102,6 +116,10 @@ interface HeadlessRunRequest {
|
|
|
102
116
|
|
|
103
117
|
// Folder of the atc-bridge mod the run's CLI loads, supplied by the adapter.
|
|
104
118
|
readonly pluginDir?: string;
|
|
119
|
+
|
|
120
|
+
// The session's own model and effort, so a headless turn keeps them.
|
|
121
|
+
readonly model?: string;
|
|
122
|
+
readonly effort?: string;
|
|
105
123
|
}
|
|
106
124
|
|
|
107
125
|
interface HeadlessRunEvents {
|
|
@@ -133,6 +151,39 @@ export interface AgentProfile {
|
|
|
133
151
|
|
|
134
152
|
// Model names the config sets explicitly, keyed by role; null when it sets none.
|
|
135
153
|
readonly models: Readonly<Record<string, string>> | null;
|
|
154
|
+
|
|
155
|
+
// The per-session overrides a spawn can pass this agent's CLI.
|
|
156
|
+
readonly spawnOptions: SpawnOptionSpecs;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A value worth offering for a spawn option, with the provider model it
|
|
161
|
+
* maps to when the config holds that mapping.
|
|
162
|
+
*/
|
|
163
|
+
interface SpawnOptionExample {
|
|
164
|
+
readonly value: string;
|
|
165
|
+
readonly resolvesTo: string | null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Whether the agent's CLI takes one spawn option, and which values it does.
|
|
170
|
+
* `values` holds the closed set a value must come from; null takes any
|
|
171
|
+
* well-formed value. `backendEffect` is `applied` when the CLI applies the
|
|
172
|
+
* value itself and `unverified` when the backend behind the CLI may ignore
|
|
173
|
+
* it; null for an option the agent does not take.
|
|
174
|
+
*/
|
|
175
|
+
export interface SpawnOptionSpec {
|
|
176
|
+
readonly supported: boolean;
|
|
177
|
+
readonly values: readonly string[] | null;
|
|
178
|
+
readonly examples: readonly SpawnOptionExample[];
|
|
179
|
+
readonly default: string | null;
|
|
180
|
+
readonly backendEffect: 'applied' | 'unverified' | null;
|
|
181
|
+
readonly note: string | null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface SpawnOptionSpecs {
|
|
185
|
+
readonly model: SpawnOptionSpec;
|
|
186
|
+
readonly effort: SpawnOptionSpec;
|
|
136
187
|
}
|
|
137
188
|
|
|
138
189
|
/**
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An argument list with every occurrence of the named flags removed, each
|
|
3
|
+
* with its value: the flag followed by its value, or `--flag=value`. Every
|
|
4
|
+
* other argument keeps its place.
|
|
5
|
+
*/
|
|
6
|
+
export function buildArgsWithoutFlags(args: readonly string[], names: readonly string[]): string[] {
|
|
7
|
+
const kept: string[] = [];
|
|
8
|
+
|
|
9
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10
|
+
const arg = args[index] ?? '';
|
|
11
|
+
|
|
12
|
+
if (names.includes(arg)) {
|
|
13
|
+
index += 1;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (names.some((name) => name.startsWith('--') && arg.startsWith(`${name}=`))) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
kept.push(arg);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return kept;
|
|
25
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { SpawnOverrides } from './agent-adapter';
|
|
2
|
+
import { buildArgsWithoutFlags } from './build-args-without-flags';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The configured leading arguments for a Claude CLI spawn with the session's
|
|
6
|
+
* model and effort applied. An override replaces any `--model` or `--effort`
|
|
7
|
+
* the configured arguments carry, value included, so the session never
|
|
8
|
+
* depends on which occurrence the CLI honours. Without overrides the
|
|
9
|
+
* configured arguments pass through unchanged. Each value travels as its own
|
|
10
|
+
* argument, never through a shell.
|
|
11
|
+
*/
|
|
12
|
+
export function buildClaudeOverrideArgs(
|
|
13
|
+
configured: readonly string[],
|
|
14
|
+
overrides: SpawnOverrides,
|
|
15
|
+
): string[] {
|
|
16
|
+
const replaced = [
|
|
17
|
+
...(overrides.model === undefined ? [] : ['--model']),
|
|
18
|
+
...(overrides.effort === undefined ? [] : ['--effort']),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
...buildArgsWithoutFlags(configured, replaced),
|
|
23
|
+
...(overrides.model === undefined ? [] : ['--model', overrides.model]),
|
|
24
|
+
...(overrides.effort === undefined ? [] : ['--effort', overrides.effort]),
|
|
25
|
+
];
|
|
26
|
+
}
|
|
@@ -14,9 +14,13 @@ import type {
|
|
|
14
14
|
HeadlessRunner,
|
|
15
15
|
NameUpdate,
|
|
16
16
|
ResumeCheck,
|
|
17
|
+
SpawnOptionSpecs,
|
|
17
18
|
SpawnOptions,
|
|
18
19
|
SpawnPlan,
|
|
19
20
|
} from './agent-adapter';
|
|
21
|
+
import { buildClaudeOverrideArgs } from './build-claude-override-args';
|
|
22
|
+
import { CLAUDE_EFFORT_LEVELS } from './claude-effort-levels';
|
|
23
|
+
import { findFlagValue } from './find-flag-value';
|
|
20
24
|
import { parseClaudeTranscriptLine } from './parse-claude-transcript-line';
|
|
21
25
|
import { truncateDetail } from './truncate-detail';
|
|
22
26
|
import { writeATCBridge } from './write-atc-bridge';
|
|
@@ -66,7 +70,14 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
66
70
|
constructor(config: Config, headlessRunner: HeadlessRunner | null = null, bridgeTarget?: string) {
|
|
67
71
|
this.bridgeTarget = bridgeTarget;
|
|
68
72
|
this.config = config;
|
|
69
|
-
|
|
73
|
+
|
|
74
|
+
this.profile = {
|
|
75
|
+
label: 'Claude',
|
|
76
|
+
kind: 'claude',
|
|
77
|
+
bin: config.claudeBin,
|
|
78
|
+
models: null,
|
|
79
|
+
spawnOptions: buildClaudeSpawnOptions(config.claudeArgs),
|
|
80
|
+
};
|
|
70
81
|
|
|
71
82
|
this.headlessRunner =
|
|
72
83
|
headlessRunner === null
|
|
@@ -80,7 +91,7 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
80
91
|
return {
|
|
81
92
|
bin: this.config.claudeBin,
|
|
82
93
|
args: [
|
|
83
|
-
...this.config.claudeArgs,
|
|
94
|
+
...buildClaudeOverrideArgs(this.config.claudeArgs, opts),
|
|
84
95
|
'--settings',
|
|
85
96
|
this.settingsFile,
|
|
86
97
|
'--plugin-dir',
|
|
@@ -230,3 +241,39 @@ export class ClaudeAdapter implements AgentAdapter {
|
|
|
230
241
|
return `cd ${toShellArg(cwd)} && ${resume}`;
|
|
231
242
|
}
|
|
232
243
|
}
|
|
244
|
+
|
|
245
|
+
// The aliases Claude Code documents for `--model`, each resolving to a model
|
|
246
|
+
// the account picks. A full model name is accepted as well.
|
|
247
|
+
const CLAUDE_MODEL_ALIASES = [
|
|
248
|
+
'best',
|
|
249
|
+
'fable',
|
|
250
|
+
'opus',
|
|
251
|
+
'sonnet',
|
|
252
|
+
'haiku',
|
|
253
|
+
'opus[1m]',
|
|
254
|
+
'sonnet[1m]',
|
|
255
|
+
'opusplan',
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
// What a Claude spawn can override. Each default is the value the configured
|
|
259
|
+
// arguments pass, or null when the CLI picks its own.
|
|
260
|
+
function buildClaudeSpawnOptions(claudeArgs: readonly string[]): SpawnOptionSpecs {
|
|
261
|
+
return {
|
|
262
|
+
model: {
|
|
263
|
+
supported: true,
|
|
264
|
+
values: null,
|
|
265
|
+
examples: CLAUDE_MODEL_ALIASES.map((value) => ({ value, resolvesTo: null })),
|
|
266
|
+
default: findFlagValue(claudeArgs, ['--model']),
|
|
267
|
+
backendEffect: 'applied',
|
|
268
|
+
note: 'An alias or a full model name, passed as --model.',
|
|
269
|
+
},
|
|
270
|
+
effort: {
|
|
271
|
+
supported: true,
|
|
272
|
+
values: CLAUDE_EFFORT_LEVELS,
|
|
273
|
+
examples: [],
|
|
274
|
+
default: findFlagValue(claudeArgs, ['--effort']),
|
|
275
|
+
backendEffect: 'applied',
|
|
276
|
+
note: 'Passed as --effort. Which levels a session honours depends on its model.',
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -14,9 +14,12 @@ import type {
|
|
|
14
14
|
AgentProfile,
|
|
15
15
|
NameUpdate,
|
|
16
16
|
ResumeCheck,
|
|
17
|
+
SpawnOptionSpecs,
|
|
17
18
|
SpawnOptions,
|
|
18
19
|
SpawnPlan,
|
|
19
20
|
} from './agent-adapter';
|
|
21
|
+
import { buildArgsWithoutFlags } from './build-args-without-flags';
|
|
22
|
+
import { findFlagValue } from './find-flag-value';
|
|
20
23
|
import { resolveAgentHome } from './resolve-agent-home';
|
|
21
24
|
import { truncateDetail } from './truncate-detail';
|
|
22
25
|
|
|
@@ -33,6 +36,9 @@ const CODEX_HOOK_PAYLOAD_SCHEMA = z.object({
|
|
|
33
36
|
|
|
34
37
|
type CodexHookPayload = z.infer<typeof CODEX_HOOK_PAYLOAD_SCHEMA>;
|
|
35
38
|
|
|
39
|
+
// The spellings of the Codex CLI's model flag.
|
|
40
|
+
const CODEX_MODEL_FLAGS = ['-m', '--model'];
|
|
41
|
+
|
|
36
42
|
/**
|
|
37
43
|
* The Codex CLI adapter: spawn arguments, hook payload mapping, resume
|
|
38
44
|
* semantics, and session_index.jsonl name-pulling. Hooks are a user-installed
|
|
@@ -56,14 +62,25 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
56
62
|
|
|
57
63
|
constructor(config: Config) {
|
|
58
64
|
this.config = config;
|
|
59
|
-
|
|
65
|
+
|
|
66
|
+
this.profile = {
|
|
67
|
+
label: 'Codex',
|
|
68
|
+
kind: 'codex',
|
|
69
|
+
bin: config.codexBin,
|
|
70
|
+
models: null,
|
|
71
|
+
spawnOptions: buildCodexSpawnOptions(config.codexArgs),
|
|
72
|
+
};
|
|
60
73
|
}
|
|
61
74
|
|
|
62
75
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
|
63
76
|
return {
|
|
64
77
|
bin: this.config.codexBin,
|
|
65
78
|
args: [
|
|
66
|
-
|
|
79
|
+
// A model override replaces any model flag the configured arguments
|
|
80
|
+
// carry, and travels as its own argument.
|
|
81
|
+
...(opts.model === undefined
|
|
82
|
+
? this.config.codexArgs
|
|
83
|
+
: [...buildArgsWithoutFlags(this.config.codexArgs, CODEX_MODEL_FLAGS), '-m', opts.model]),
|
|
67
84
|
|
|
68
85
|
// Bare resume opens Codex's own session picker; an id resumes that
|
|
69
86
|
// session directly. Both accept a prompt afterwards.
|
|
@@ -193,3 +210,27 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
193
210
|
return `cd ${toShellArg(cwd)} && ${resume}`;
|
|
194
211
|
}
|
|
195
212
|
}
|
|
213
|
+
|
|
214
|
+
// What a Codex spawn can override. Codex documents its reasoning effort as
|
|
215
|
+
// whatever the selected model advertises, with no closed list of levels, so
|
|
216
|
+
// atc takes no effort for it.
|
|
217
|
+
function buildCodexSpawnOptions(codexArgs: readonly string[]): SpawnOptionSpecs {
|
|
218
|
+
return {
|
|
219
|
+
model: {
|
|
220
|
+
supported: true,
|
|
221
|
+
values: null,
|
|
222
|
+
examples: [],
|
|
223
|
+
default: findFlagValue(codexArgs, CODEX_MODEL_FLAGS),
|
|
224
|
+
backendEffect: 'applied',
|
|
225
|
+
note: 'A model name, passed as -m.',
|
|
226
|
+
},
|
|
227
|
+
effort: {
|
|
228
|
+
supported: false,
|
|
229
|
+
values: null,
|
|
230
|
+
examples: [],
|
|
231
|
+
default: null,
|
|
232
|
+
backendEffect: null,
|
|
233
|
+
note: 'Codex documents its reasoning effort levels as depending on the model, with no closed list, so atc does not pass one.',
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The value a CLI flag takes in an argument list, written either as the flag
|
|
3
|
+
* followed by its value or as `--flag=value`. The last occurrence wins, as it
|
|
4
|
+
* does for the CLIs atc runs; null when the list never sets the flag.
|
|
5
|
+
*/
|
|
6
|
+
export function findFlagValue(args: readonly string[], names: readonly string[]): string | null {
|
|
7
|
+
let value: string | null = null;
|
|
8
|
+
|
|
9
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10
|
+
const arg = args[index] ?? '';
|
|
11
|
+
const next = args[index + 1];
|
|
12
|
+
|
|
13
|
+
if (names.includes(arg) && next !== undefined) {
|
|
14
|
+
value = next;
|
|
15
|
+
index += 1;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const inline = names.find((name) => name.startsWith('--') && arg.startsWith(`${name}=`));
|
|
20
|
+
|
|
21
|
+
if (inline !== undefined) {
|
|
22
|
+
value = arg.slice(inline.length + 1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
@@ -11,10 +11,14 @@ import type {
|
|
|
11
11
|
HeadlessRunner,
|
|
12
12
|
NameUpdate,
|
|
13
13
|
ResumeCheck,
|
|
14
|
+
SpawnOptionSpecs,
|
|
14
15
|
SpawnOptions,
|
|
15
16
|
SpawnPlan,
|
|
16
17
|
} from './agent-adapter';
|
|
18
|
+
import { buildClaudeOverrideArgs } from './build-claude-override-args';
|
|
17
19
|
import { ClaudeAdapter } from './claude-adapter';
|
|
20
|
+
import { CLAUDE_EFFORT_LEVELS } from './claude-effort-levels';
|
|
21
|
+
import { findFlagValue } from './find-flag-value';
|
|
18
22
|
import { parseClaudeTranscriptLine } from './parse-claude-transcript-line';
|
|
19
23
|
import { writeATCBridge } from './write-atc-bridge';
|
|
20
24
|
import { writeHookSettings } from './write-hook-settings';
|
|
@@ -65,11 +69,14 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
65
69
|
this.gateway = gateway;
|
|
66
70
|
this.id = gateway.id;
|
|
67
71
|
|
|
72
|
+
const models = pickModels(gateway.env);
|
|
73
|
+
|
|
68
74
|
this.profile = {
|
|
69
75
|
label: gateway.label,
|
|
70
76
|
kind: 'gateway',
|
|
71
77
|
bin: gateway.bin,
|
|
72
|
-
models
|
|
78
|
+
models,
|
|
79
|
+
spawnOptions: buildGatewaySpawnOptions(gateway.args, models),
|
|
73
80
|
};
|
|
74
81
|
|
|
75
82
|
this.claude = new ClaudeAdapter(config);
|
|
@@ -88,7 +95,7 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
88
95
|
return {
|
|
89
96
|
bin: this.gateway.bin,
|
|
90
97
|
args: [
|
|
91
|
-
...this.gateway.args,
|
|
98
|
+
...buildClaudeOverrideArgs(this.gateway.args, opts),
|
|
92
99
|
'--settings',
|
|
93
100
|
this.writeSettings(),
|
|
94
101
|
'--plugin-dir',
|
|
@@ -162,3 +169,39 @@ function pickModels(
|
|
|
162
169
|
|
|
163
170
|
return Object.keys(models).length === 0 ? null : models;
|
|
164
171
|
}
|
|
172
|
+
|
|
173
|
+
// The Claude tiers a gateway's env can map to a provider model.
|
|
174
|
+
const GATEWAY_TIERS = ['opus', 'sonnet', 'haiku'];
|
|
175
|
+
|
|
176
|
+
// What a gateway spawn can override. The model examples are the tier aliases
|
|
177
|
+
// the gateway's env maps, each with the provider model it reaches. Effort
|
|
178
|
+
// goes to the CLI unchanged, and whether the provider acts on it is unknown.
|
|
179
|
+
function buildGatewaySpawnOptions(
|
|
180
|
+
args: readonly string[],
|
|
181
|
+
models: Readonly<Record<string, string>> | null,
|
|
182
|
+
): SpawnOptionSpecs {
|
|
183
|
+
const examples = GATEWAY_TIERS.flatMap((tier) => {
|
|
184
|
+
const mapped = models?.[tier];
|
|
185
|
+
|
|
186
|
+
return mapped === undefined ? [] : [{ value: tier, resolvesTo: mapped }];
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
model: {
|
|
191
|
+
supported: true,
|
|
192
|
+
values: null,
|
|
193
|
+
examples,
|
|
194
|
+
default: findFlagValue(args, ['--model']) ?? models?.['default'] ?? null,
|
|
195
|
+
backendEffect: 'applied',
|
|
196
|
+
note: "A tier alias the gateway's env maps, or a model name the provider accepts, passed as --model.",
|
|
197
|
+
},
|
|
198
|
+
effort: {
|
|
199
|
+
supported: true,
|
|
200
|
+
values: CLAUDE_EFFORT_LEVELS,
|
|
201
|
+
examples: [],
|
|
202
|
+
default: findFlagValue(args, ['--effort']),
|
|
203
|
+
backendEffect: 'unverified',
|
|
204
|
+
note: "Passed as --effort; the gateway's provider may ignore it.",
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
AgentProfile,
|
|
16
16
|
NameUpdate,
|
|
17
17
|
ResumeCheck,
|
|
18
|
+
SpawnOptionSpec,
|
|
18
19
|
SpawnOptions,
|
|
19
20
|
SpawnPlan,
|
|
20
21
|
} from './agent-adapter';
|
|
@@ -48,6 +49,16 @@ const GROK_HOOK_PAYLOAD_SCHEMA = z.object({
|
|
|
48
49
|
|
|
49
50
|
type GrokHookPayload = z.infer<typeof GROK_HOOK_PAYLOAD_SCHEMA>;
|
|
50
51
|
|
|
52
|
+
// atc passes Grok neither a model nor an effort; the CLI's own config picks both.
|
|
53
|
+
const GROK_UNSUPPORTED_OPTION: SpawnOptionSpec = {
|
|
54
|
+
supported: false,
|
|
55
|
+
values: null,
|
|
56
|
+
examples: [],
|
|
57
|
+
default: null,
|
|
58
|
+
backendEffect: null,
|
|
59
|
+
note: 'atc does not pass this option to Grok.',
|
|
60
|
+
};
|
|
61
|
+
|
|
51
62
|
/**
|
|
52
63
|
* The Grok Build adapter: spawn arguments, hook payload mapping,
|
|
53
64
|
* resume semantics, and summary.json name-pulling.
|
|
@@ -71,7 +82,14 @@ export class GrokAdapter implements AgentAdapter {
|
|
|
71
82
|
|
|
72
83
|
constructor(config: Config) {
|
|
73
84
|
this.config = config;
|
|
74
|
-
|
|
85
|
+
|
|
86
|
+
this.profile = {
|
|
87
|
+
label: 'Grok',
|
|
88
|
+
kind: 'grok',
|
|
89
|
+
bin: config.grokBin,
|
|
90
|
+
models: null,
|
|
91
|
+
spawnOptions: { model: GROK_UNSUPPORTED_OPTION, effort: GROK_UNSUPPORTED_OPTION },
|
|
92
|
+
};
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
package/src/cli.ts
CHANGED
|
@@ -160,12 +160,26 @@ const main = defineCommand({
|
|
|
160
160
|
const codex = await import('./agents/codex-adapter');
|
|
161
161
|
const gateway = await import('./agents/gateway-adapter');
|
|
162
162
|
const headless = await import('./daemon/start-headless-run');
|
|
163
|
+
const targets = await import('./daemon/build-execution-targets');
|
|
163
164
|
|
|
164
165
|
// Test harnesses shrink the outbound queue to force overflow
|
|
165
166
|
// deterministically; unset means the production default.
|
|
166
167
|
const queueBytes = Number(process.env['ATC_QUEUE_BYTES']);
|
|
167
168
|
const cfg = config.loadConfig();
|
|
168
169
|
|
|
170
|
+
for (const error of cfg.targetErrors) {
|
|
171
|
+
const line =
|
|
172
|
+
error.scope === 'config'
|
|
173
|
+
? `${error.path} cannot be used (${error.problem}: ${error.detail}); every spawn is refused, local ones included, until it is fixed`
|
|
174
|
+
: error.problem;
|
|
175
|
+
|
|
176
|
+
console.error(`atc daemon: config: ${line}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (const problem of cfg.principalErrors) {
|
|
180
|
+
console.error(`atc daemon: config: ${problem}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
169
183
|
// Cap on how long a fleet restore waits for one revived session to
|
|
170
184
|
// report it has booted before moving to the next. Tests pin it to
|
|
171
185
|
// keep timing deterministic; unset means the production default.
|
|
@@ -206,6 +220,10 @@ const main = defineCommand({
|
|
|
206
220
|
legacyFleetPath: config.legacyFleetFile,
|
|
207
221
|
pidPath: config.daemonPidFile,
|
|
208
222
|
hooks: cfg.hooks,
|
|
223
|
+
targets: targets.buildExecutionTargets(cfg.targets),
|
|
224
|
+
defaultTarget: cfg.defaultTarget,
|
|
225
|
+
targetErrors: cfg.targetErrors,
|
|
226
|
+
principals: cfg.principals,
|
|
209
227
|
restoreBootTimeoutMs,
|
|
210
228
|
...(Number.isFinite(graceOverride) && graceOverride >= 0
|
|
211
229
|
? { tapGraceMs: graceOverride }
|
|
@@ -65,9 +65,12 @@ export class DaemonClient {
|
|
|
65
65
|
return this.sendRequest('daemon.hello', { client: build, auth: { scheme: 'none' } });
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// A request with a principal acts as that principal, within what the
|
|
69
|
+
// connection may reach.
|
|
68
70
|
sendRequest(
|
|
69
71
|
m: string,
|
|
70
72
|
p?: Readonly<Record<string, unknown>>,
|
|
73
|
+
as?: string,
|
|
71
74
|
): Promise<Readonly<Record<string, unknown>>> {
|
|
72
75
|
if (this.closedReason !== null) {
|
|
73
76
|
return Promise.reject(new DaemonError('internal', this.closedReason));
|
|
@@ -77,7 +80,16 @@ export class DaemonClient {
|
|
|
77
80
|
const resolvers = Promise.withResolvers<Readonly<Record<string, unknown>>>();
|
|
78
81
|
|
|
79
82
|
this.pending.set(id, { resolve: resolvers.resolve, reject: resolvers.reject });
|
|
80
|
-
|
|
83
|
+
|
|
84
|
+
this.queue?.send(
|
|
85
|
+
encodeMessage({
|
|
86
|
+
v: PROTOCOL_V,
|
|
87
|
+
id,
|
|
88
|
+
m,
|
|
89
|
+
...(p === undefined ? {} : { p }),
|
|
90
|
+
...(as === undefined ? {} : { as }),
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
81
93
|
|
|
82
94
|
return resolvers.promise;
|
|
83
95
|
}
|
|
@@ -138,7 +150,7 @@ export class DaemonClient {
|
|
|
138
150
|
if (msg.err === undefined) {
|
|
139
151
|
waiter.resolve(msg.ok ?? {});
|
|
140
152
|
} else {
|
|
141
|
-
waiter.reject(new DaemonError(msg.err.code, msg.err.msg));
|
|
153
|
+
waiter.reject(new DaemonError(msg.err.code, msg.err.msg, msg.err.data));
|
|
142
154
|
}
|
|
143
155
|
}
|
|
144
156
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import type { AgentID } from '../agents/agent-adapter';
|
|
3
|
+
import { DaemonError } from '../protocol/daemon-error';
|
|
3
4
|
import { loadConfig } from '../shared/config';
|
|
4
5
|
import { collectAgentPicks } from './collect-agent-picks';
|
|
5
6
|
import type { AgentPick } from './collect-agent-picks';
|
|
@@ -32,7 +33,8 @@ export interface SpawnPickerDeps<TMirror> {
|
|
|
32
33
|
/**
|
|
33
34
|
* The modal flow behind n and r: agent, then directory, then name, then an
|
|
34
35
|
* optional first prompt. Every path out of it either attaches the new
|
|
35
|
-
* session or returns the client to the screen it came from.
|
|
36
|
+
* session or returns the client to the screen it came from. A spawn the
|
|
37
|
+
* daemon refuses is no path out: the flow stays open and shows why.
|
|
36
38
|
*/
|
|
37
39
|
export class SpawnPicker<TMirror extends { readonly id: string }> {
|
|
38
40
|
private readonly deps: SpawnPickerDeps<TMirror>;
|
|
@@ -60,6 +62,10 @@ export class SpawnPicker<TMirror extends { readonly id: string }> {
|
|
|
60
62
|
|
|
61
63
|
private agent: AgentID = 'claude';
|
|
62
64
|
|
|
65
|
+
// The daemon's refusal of the last spawn, shown in place of the hint until
|
|
66
|
+
// the next key.
|
|
67
|
+
private refusal: string | null = null;
|
|
68
|
+
|
|
63
69
|
constructor(deps: SpawnPickerDeps<TMirror>) {
|
|
64
70
|
this.deps = deps;
|
|
65
71
|
}
|
|
@@ -82,12 +88,15 @@ export class SpawnPicker<TMirror extends { readonly id: string }> {
|
|
|
82
88
|
|
|
83
89
|
this.agent = this.picks[this.selected]?.agent ?? this.deps.getLastUsedAgent();
|
|
84
90
|
this.step = 'agent';
|
|
91
|
+
this.refusal = null;
|
|
85
92
|
|
|
86
93
|
process.stdout.write(ansi.clear);
|
|
87
94
|
this.render();
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
applyKey(buf: Buffer) {
|
|
98
|
+
this.refusal = null;
|
|
99
|
+
|
|
91
100
|
const edit = planTextEdit(buf, this.input, {
|
|
92
101
|
isLeaderKey: this.deps.isLeaderKey,
|
|
93
102
|
moves: this.step === 'agent' || this.step === 'dir',
|
|
@@ -168,7 +177,7 @@ export class SpawnPicker<TMirror extends { readonly id: string }> {
|
|
|
168
177
|
selected: -1,
|
|
169
178
|
input: this.input,
|
|
170
179
|
placeholder: formatDirName(this.dir),
|
|
171
|
-
hint: `session name for ${formatDir(this.dir)} · ⏎ accept · esc back`,
|
|
180
|
+
hint: this.refusal ?? `session name for ${formatDir(this.dir)} · ⏎ accept · esc back`,
|
|
172
181
|
});
|
|
173
182
|
} else {
|
|
174
183
|
drawPicker({
|
|
@@ -177,7 +186,7 @@ export class SpawnPicker<TMirror extends { readonly id: string }> {
|
|
|
177
186
|
selected: -1,
|
|
178
187
|
input: this.input,
|
|
179
188
|
placeholder: 'optional — ⏎ to start interactive',
|
|
180
|
-
hint: 'first message for the session · ⏎ spawn · esc back',
|
|
189
|
+
hint: this.refusal ?? 'first message for the session · ⏎ spawn · esc back',
|
|
181
190
|
});
|
|
182
191
|
}
|
|
183
192
|
|
|
@@ -320,7 +329,21 @@ export class SpawnPicker<TMirror extends { readonly id: string }> {
|
|
|
320
329
|
|
|
321
330
|
return;
|
|
322
331
|
}
|
|
323
|
-
} catch {
|
|
332
|
+
} catch (error) {
|
|
333
|
+
// A refused spawn keeps the picker on the step that sent it, with the
|
|
334
|
+
// entered text back in the input, so the user can fix the cause and
|
|
335
|
+
// retry or back out.
|
|
336
|
+
const reason =
|
|
337
|
+
error instanceof DaemonError ? `${error.code}: ${error.message}` : String(error);
|
|
338
|
+
|
|
339
|
+
this.refusal = `${reason} · ⏎ retry · esc back`;
|
|
340
|
+
this.input = this.resume ? this.name : prompt;
|
|
341
|
+
|
|
342
|
+
process.stdout.write(ansi.clear);
|
|
343
|
+
this.render();
|
|
344
|
+
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
324
347
|
|
|
325
348
|
this.deps.toBase();
|
|
326
349
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentAdapter } from '../agents/agent-adapter';
|
|
1
|
+
import type { AgentAdapter, SpawnOptionSpec } from '../agents/agent-adapter';
|
|
2
2
|
|
|
3
3
|
interface AgentCapabilities {
|
|
4
4
|
// Only an installed agent can start a session.
|
|
@@ -21,6 +21,19 @@ export interface AgentEntry {
|
|
|
21
21
|
readonly installed: boolean;
|
|
22
22
|
readonly capabilities: AgentCapabilities;
|
|
23
23
|
readonly models: Readonly<Record<string, string>> | null;
|
|
24
|
+
readonly spawnOptions: SpawnOptionEntries;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A spawn option as `agents.list` shows it: the agent's own spec, plus
|
|
28
|
+
// whether a spawn on this host can pass it now. Only an installed agent's
|
|
29
|
+
// supported option is available.
|
|
30
|
+
export interface SpawnOptionEntry extends SpawnOptionSpec {
|
|
31
|
+
readonly available: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SpawnOptionEntries {
|
|
35
|
+
readonly model: SpawnOptionEntry;
|
|
36
|
+
readonly effort: SpawnOptionEntry;
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
/**
|
|
@@ -53,6 +66,29 @@ export function buildAgentList(
|
|
|
53
66
|
input: true,
|
|
54
67
|
},
|
|
55
68
|
models: profile?.models ?? null,
|
|
69
|
+
spawnOptions: {
|
|
70
|
+
model: buildSpawnOptionEntry(profile?.spawnOptions.model, installed),
|
|
71
|
+
effort: buildSpawnOptionEntry(profile?.spawnOptions.effort, installed),
|
|
72
|
+
},
|
|
56
73
|
};
|
|
57
74
|
});
|
|
58
75
|
}
|
|
76
|
+
|
|
77
|
+
// A stand-in adapter declares no spawn options, so it takes none.
|
|
78
|
+
const NO_SPAWN_OPTION: SpawnOptionSpec = {
|
|
79
|
+
supported: false,
|
|
80
|
+
values: null,
|
|
81
|
+
examples: [],
|
|
82
|
+
default: null,
|
|
83
|
+
backendEffect: null,
|
|
84
|
+
note: null,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function buildSpawnOptionEntry(
|
|
88
|
+
spec: SpawnOptionSpec | undefined,
|
|
89
|
+
installed: boolean,
|
|
90
|
+
): SpawnOptionEntry {
|
|
91
|
+
const resolved = spec ?? NO_SPAWN_OPTION;
|
|
92
|
+
|
|
93
|
+
return { ...resolved, available: installed && resolved.supported };
|
|
94
|
+
}
|