@zgeoff/atc 2.11.0 → 2.13.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 +10 -10
- package/package.json +2 -1
- package/src/agents/agent-adapter.ts +98 -10
- package/src/agents/build-args-without-flags.ts +25 -0
- package/src/agents/build-atc-bridge-files.ts +13 -0
- package/src/agents/build-claude-override-args.ts +26 -0
- package/src/agents/build-claude-query-options.ts +72 -0
- package/src/agents/build-cli-command.ts +7 -5
- package/src/{daemon → agents}/build-headless-env.ts +12 -6
- package/src/agents/build-hook-settings.ts +8 -2
- package/src/agents/build-restore-mode-args.ts +23 -0
- package/src/agents/claude-adapter.ts +112 -18
- package/src/agents/claude-effort-levels.ts +5 -0
- package/src/agents/codex-adapter.ts +48 -2
- package/src/agents/find-claude-permission-mode.ts +24 -0
- package/src/agents/find-flag-value.ts +27 -0
- package/src/agents/gateway-adapter.ts +83 -13
- package/src/agents/grok-adapter.ts +24 -1
- package/src/agents/make-claude-headless-runner.ts +46 -0
- package/src/agents/plan-pasted-line-input.ts +22 -0
- package/src/agents/plan-typed-line-input.ts +8 -0
- package/src/agents/resolve-claude-permission-mode.ts +13 -0
- package/src/{daemon/start-headless-run.ts → agents/start-claude-headless-run.ts} +6 -42
- package/src/agents/write-atc-bridge.ts +2 -6
- package/src/cli.ts +23 -9
- package/src/client/daemon-client.ts +14 -2
- package/src/client/index.ts +50 -1
- package/src/client/spawn-picker.ts +27 -4
- package/src/client/ui.ts +8 -0
- 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 +73 -0
- package/src/daemon/build-fleet-events.ts +2 -1
- package/src/daemon/build-imp-provider.ts +68 -0
- package/src/daemon/build-payload-hash.ts +31 -0
- package/src/daemon/build-report-trail-entry.ts +4 -2
- package/src/daemon/build-scoped-context.ts +235 -0
- package/src/daemon/build-session-lifecycle.ts +52 -0
- package/src/daemon/build-tar-archive.ts +85 -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 +425 -96
- package/src/daemon/daemon.ts +730 -96
- package/src/daemon/effect-remains-error.ts +13 -0
- package/src/daemon/execution-provider.ts +208 -0
- package/src/daemon/find-execution-refusal.ts +104 -0
- package/src/daemon/hooks.ts +5 -17
- package/src/daemon/idempotency-ledger.ts +164 -0
- package/src/daemon/imp-client-port.ts +343 -0
- package/src/daemon/imp-harness.ts +618 -0
- package/src/daemon/imp-port-error.ts +18 -0
- package/src/daemon/imp-port.ts +246 -0
- package/src/daemon/imp-provider.ts +601 -0
- package/src/daemon/is-binding-current.ts +43 -0
- package/src/daemon/local-pty-provider.ts +142 -0
- package/src/daemon/materialize-workspace.ts +571 -0
- package/src/daemon/mint-session-id.ts +5 -5
- package/src/daemon/parse-hook-line.ts +31 -0
- package/src/daemon/parse-spawn-overrides.ts +94 -0
- package/src/daemon/permission-registry.ts +14 -4
- package/src/daemon/pick-session-state.ts +15 -0
- package/src/daemon/restore-fleet.ts +103 -38
- package/src/daemon/screen-model.ts +7 -0
- package/src/daemon/session-runtime.ts +10 -0
- package/src/daemon/sessions.ts +835 -105
- package/src/daemon/start-headless-turn.ts +12 -3
- package/src/daemon/start-session-bridge.ts +294 -0
- 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 +107 -10
- package/src/mcp/parse-idempotency-key.ts +29 -0
- package/src/mcp/reconnecting-caller.ts +39 -7
- package/src/mcp/require-daemon-features.ts +10 -0
- package/src/mcp/run-tool.ts +87 -16
- package/src/mcp/types.ts +2 -0
- package/src/protocol/daemon-error.ts +5 -1
- package/src/protocol/daemon-features.ts +35 -0
- package/src/protocol/protocol.ts +67 -11
- package/src/protocol/request-param-schemas.ts +126 -10
- package/src/report.ts +37 -2
- package/src/run-bridge-tap.ts +241 -0
- package/src/shared/agent-session-id.ts +1 -1
- package/src/shared/collect-clean-env.ts +9 -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/open-bridge-socket.ts +86 -0
- package/src/shared/send-bridge-request.ts +47 -0
- package/src/shared/sort-json-keys.ts +19 -0
- package/src/shared/to-daemon-id.ts +11 -0
- package/src/statusline.ts +34 -3
- package/src/store/fleet-entry.ts +61 -9
- package/src/store/idempotency-record.ts +48 -0
- package/src/store/message-owner.ts +1 -1
- package/src/store/run-migrations.ts +330 -5
- package/src/store/state-store.ts +607 -44
- package/src/store/trail-entry.ts +4 -0
- package/src/store/workspace-materialization.ts +70 -0
- package/src/tap.ts +11 -0
- package/src/workspace/check-url-credentials.ts +59 -0
- package/src/workspace/check-workspace-completeness.ts +90 -0
- package/src/workspace/create-workspace-clone.ts +251 -0
- package/src/workspace/normalize-git-url.ts +59 -0
- package/src/workspace/read-workspace-tar.ts +38 -0
- package/src/workspace/repository-env-vars.ts +22 -0
- package/src/workspace/resolve-path-source.ts +170 -0
- package/src/workspace/run-git.ts +87 -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/src/{daemon → agents}/resolve-headless-executable.ts +0 -0
|
@@ -14,9 +14,13 @@ 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';
|
|
23
|
+
import { planPastedLineInput } from './plan-pasted-line-input';
|
|
20
24
|
import { resolveAgentHome } from './resolve-agent-home';
|
|
21
25
|
import { truncateDetail } from './truncate-detail';
|
|
22
26
|
|
|
@@ -33,6 +37,9 @@ const CODEX_HOOK_PAYLOAD_SCHEMA = z.object({
|
|
|
33
37
|
|
|
34
38
|
type CodexHookPayload = z.infer<typeof CODEX_HOOK_PAYLOAD_SCHEMA>;
|
|
35
39
|
|
|
40
|
+
// The spellings of the Codex CLI's model flag.
|
|
41
|
+
const CODEX_MODEL_FLAGS = ['-m', '--model'];
|
|
42
|
+
|
|
36
43
|
/**
|
|
37
44
|
* The Codex CLI adapter: spawn arguments, hook payload mapping, resume
|
|
38
45
|
* semantics, and session_index.jsonl name-pulling. Hooks are a user-installed
|
|
@@ -52,18 +59,33 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
52
59
|
|
|
53
60
|
readonly takesMessages = false;
|
|
54
61
|
|
|
62
|
+
// Codex's TUI keeps a newline that arrives inside a burst of input, so a
|
|
63
|
+
// line is pasted and then submitted.
|
|
64
|
+
readonly planLineInput = planPastedLineInput;
|
|
65
|
+
|
|
55
66
|
private readonly config: Config;
|
|
56
67
|
|
|
57
68
|
constructor(config: Config) {
|
|
58
69
|
this.config = config;
|
|
59
|
-
|
|
70
|
+
|
|
71
|
+
this.profile = {
|
|
72
|
+
label: 'Codex',
|
|
73
|
+
kind: 'codex',
|
|
74
|
+
bin: config.codexBin,
|
|
75
|
+
models: null,
|
|
76
|
+
spawnOptions: buildCodexSpawnOptions(config.codexArgs),
|
|
77
|
+
};
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
|
63
81
|
return {
|
|
64
82
|
bin: this.config.codexBin,
|
|
65
83
|
args: [
|
|
66
|
-
|
|
84
|
+
// A model override replaces any model flag the configured arguments
|
|
85
|
+
// carry, and travels as its own argument.
|
|
86
|
+
...(opts.model === undefined
|
|
87
|
+
? this.config.codexArgs
|
|
88
|
+
: [...buildArgsWithoutFlags(this.config.codexArgs, CODEX_MODEL_FLAGS), '-m', opts.model]),
|
|
67
89
|
|
|
68
90
|
// Bare resume opens Codex's own session picker; an id resumes that
|
|
69
91
|
// session directly. Both accept a prompt afterwards.
|
|
@@ -193,3 +215,27 @@ export class CodexAdapter implements AgentAdapter {
|
|
|
193
215
|
return `cd ${toShellArg(cwd)} && ${resume}`;
|
|
194
216
|
}
|
|
195
217
|
}
|
|
218
|
+
|
|
219
|
+
// What a Codex spawn can override. Codex documents its reasoning effort as
|
|
220
|
+
// whatever the selected model advertises, with no closed list of levels, so
|
|
221
|
+
// atc takes no effort for it.
|
|
222
|
+
function buildCodexSpawnOptions(codexArgs: readonly string[]): SpawnOptionSpecs {
|
|
223
|
+
return {
|
|
224
|
+
model: {
|
|
225
|
+
supported: true,
|
|
226
|
+
values: null,
|
|
227
|
+
examples: [],
|
|
228
|
+
default: findFlagValue(codexArgs, CODEX_MODEL_FLAGS),
|
|
229
|
+
backendEffect: 'applied',
|
|
230
|
+
note: 'A model name, passed as -m.',
|
|
231
|
+
},
|
|
232
|
+
effort: {
|
|
233
|
+
supported: false,
|
|
234
|
+
values: null,
|
|
235
|
+
examples: [],
|
|
236
|
+
default: null,
|
|
237
|
+
backendEffect: null,
|
|
238
|
+
note: 'Codex documents its reasoning effort levels as depending on the model, with no closed list, so atc does not pass one.',
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isRecord } from '../shared/report';
|
|
2
|
+
import { findFlagValue } from './find-flag-value';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The permission mode a Claude CLI agent's configuration sets: an explicit
|
|
6
|
+
* `--permission-mode` in its arguments, else the `permissions.defaultMode`
|
|
7
|
+
* its settings hold, or null when neither sets one. The argument wins, as it
|
|
8
|
+
* does for the CLI itself.
|
|
9
|
+
*/
|
|
10
|
+
export function findClaudePermissionMode(
|
|
11
|
+
args: readonly string[],
|
|
12
|
+
settings: Readonly<Record<string, unknown>> | undefined,
|
|
13
|
+
): string | null {
|
|
14
|
+
const flag = findFlagValue(args, ['--permission-mode']);
|
|
15
|
+
|
|
16
|
+
if (flag !== null) {
|
|
17
|
+
return flag;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const permissions = settings?.['permissions'];
|
|
21
|
+
const defaultMode = isRecord(permissions) ? permissions['defaultMode'] : undefined;
|
|
22
|
+
|
|
23
|
+
return typeof defaultMode === 'string' && defaultMode !== '' ? defaultMode : null;
|
|
24
|
+
}
|
|
@@ -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,11 +11,20 @@ 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';
|
|
19
|
+
import { buildRestoreModeArgs } from './build-restore-mode-args';
|
|
17
20
|
import { ClaudeAdapter } from './claude-adapter';
|
|
21
|
+
import { CLAUDE_EFFORT_LEVELS } from './claude-effort-levels';
|
|
22
|
+
import { findFlagValue } from './find-flag-value';
|
|
23
|
+
import { makeClaudeHeadlessRunner } from './make-claude-headless-runner';
|
|
24
|
+
import type { ClaudeHeadlessRun } from './make-claude-headless-runner';
|
|
18
25
|
import { parseClaudeTranscriptLine } from './parse-claude-transcript-line';
|
|
26
|
+
import { planTypedLineInput } from './plan-typed-line-input';
|
|
27
|
+
import { resolveClaudePermissionMode } from './resolve-claude-permission-mode';
|
|
19
28
|
import { writeATCBridge } from './write-atc-bridge';
|
|
20
29
|
import { writeHookSettings } from './write-hook-settings';
|
|
21
30
|
|
|
@@ -39,6 +48,9 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
39
48
|
// The gateway runs the Claude CLI, which writes the same transcript.
|
|
40
49
|
readonly parseTranscriptLine = parseClaudeTranscriptLine;
|
|
41
50
|
|
|
51
|
+
// The gateway runs the Claude CLI, whose TUI takes a line the same way.
|
|
52
|
+
readonly planLineInput = planTypedLineInput;
|
|
53
|
+
|
|
42
54
|
readonly takesMessages = true;
|
|
43
55
|
|
|
44
56
|
readonly profile: AgentProfile;
|
|
@@ -58,37 +70,44 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
58
70
|
constructor(
|
|
59
71
|
gateway: GatewayConfig,
|
|
60
72
|
config: Config,
|
|
61
|
-
|
|
73
|
+
headlessRun: ClaudeHeadlessRun | null = null,
|
|
62
74
|
bridgeTarget?: string,
|
|
63
75
|
) {
|
|
64
76
|
this.bridgeTarget = bridgeTarget;
|
|
65
77
|
this.gateway = gateway;
|
|
66
78
|
this.id = gateway.id;
|
|
67
79
|
|
|
80
|
+
const models = pickModels(gateway.env);
|
|
81
|
+
|
|
68
82
|
this.profile = {
|
|
69
83
|
label: gateway.label,
|
|
70
84
|
kind: 'gateway',
|
|
71
85
|
bin: gateway.bin,
|
|
72
|
-
models
|
|
86
|
+
models,
|
|
87
|
+
spawnOptions: buildGatewaySpawnOptions(gateway.args, models),
|
|
73
88
|
};
|
|
74
89
|
|
|
75
90
|
this.claude = new ClaudeAdapter(config);
|
|
76
91
|
|
|
77
92
|
this.headlessRunner =
|
|
78
|
-
|
|
93
|
+
headlessRun === null
|
|
79
94
|
? null
|
|
80
|
-
: (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
95
|
+
: makeClaudeHeadlessRunner(headlessRun, {
|
|
96
|
+
claudeBin: gateway.bin,
|
|
97
|
+
permissionMode: resolveClaudePermissionMode(gateway.args, gateway.settings),
|
|
98
|
+
pluginDir: () => this.writeBridge(),
|
|
99
|
+
settings: () => this.writeSettings(),
|
|
100
|
+
});
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
|
88
104
|
return {
|
|
89
105
|
bin: this.gateway.bin,
|
|
90
106
|
args: [
|
|
91
|
-
...this.gateway.args,
|
|
107
|
+
...buildClaudeOverrideArgs(this.gateway.args, opts),
|
|
108
|
+
...(opts.resume === false
|
|
109
|
+
? []
|
|
110
|
+
: buildRestoreModeArgs(this.gateway.args, this.gateway.settings)),
|
|
92
111
|
'--settings',
|
|
93
112
|
this.writeSettings(),
|
|
94
113
|
'--plugin-dir',
|
|
@@ -100,6 +119,12 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
100
119
|
};
|
|
101
120
|
}
|
|
102
121
|
|
|
122
|
+
// The credential helper runs on the daemon's machine, so a gateway
|
|
123
|
+
// session never runs on a remote host.
|
|
124
|
+
planGuestSpawn(): null {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
103
128
|
normalizeHook(e: HookEvent): AdapterEvent {
|
|
104
129
|
return this.claude.normalizeHook(e);
|
|
105
130
|
}
|
|
@@ -112,14 +137,23 @@ export class GatewayAdapter implements AgentAdapter {
|
|
|
112
137
|
return this.claude.canResume(session);
|
|
113
138
|
}
|
|
114
139
|
|
|
115
|
-
// Shell command that re-opens this session outside atc. It
|
|
116
|
-
//
|
|
117
|
-
//
|
|
140
|
+
// Shell command that re-opens this session outside atc. It carries the
|
|
141
|
+
// gateway's configured arguments and its configured permission mode as an
|
|
142
|
+
// explicit flag, so that mode overrides the one the CLI would restore, and
|
|
143
|
+
// the generated settings file, because
|
|
144
|
+
// without it the CLI would resume the session against the default backend.
|
|
118
145
|
buildResumeCommand(cwd: string, agentSessionID: AgentSessionID | undefined): string | null {
|
|
146
|
+
const args = [
|
|
147
|
+
...this.gateway.args,
|
|
148
|
+
...buildRestoreModeArgs(this.gateway.args, this.gateway.settings),
|
|
149
|
+
]
|
|
150
|
+
.map((arg) => ` ${toShellArg(arg)}`)
|
|
151
|
+
.join('');
|
|
152
|
+
|
|
119
153
|
const settings = toShellArg(this.writeSettings());
|
|
120
154
|
const resume = agentSessionID === undefined ? '' : ` ${agentSessionID}`;
|
|
121
155
|
|
|
122
|
-
return `cd ${toShellArg(cwd)} && ${this.gateway.bin} --settings ${settings} --resume${resume}`;
|
|
156
|
+
return `cd ${toShellArg(cwd)} && ${this.gateway.bin}${args} --settings ${settings} --resume${resume}`;
|
|
123
157
|
}
|
|
124
158
|
|
|
125
159
|
private writeSettings(): string {
|
|
@@ -162,3 +196,39 @@ function pickModels(
|
|
|
162
196
|
|
|
163
197
|
return Object.keys(models).length === 0 ? null : models;
|
|
164
198
|
}
|
|
199
|
+
|
|
200
|
+
// The Claude tiers a gateway's env can map to a provider model.
|
|
201
|
+
const GATEWAY_TIERS = ['opus', 'sonnet', 'haiku'];
|
|
202
|
+
|
|
203
|
+
// What a gateway spawn can override. The model examples are the tier aliases
|
|
204
|
+
// the gateway's env maps, each with the provider model it reaches. Effort
|
|
205
|
+
// goes to the CLI unchanged, and whether the provider acts on it is unknown.
|
|
206
|
+
function buildGatewaySpawnOptions(
|
|
207
|
+
args: readonly string[],
|
|
208
|
+
models: Readonly<Record<string, string>> | null,
|
|
209
|
+
): SpawnOptionSpecs {
|
|
210
|
+
const examples = GATEWAY_TIERS.flatMap((tier) => {
|
|
211
|
+
const mapped = models?.[tier];
|
|
212
|
+
|
|
213
|
+
return mapped === undefined ? [] : [{ value: tier, resolvesTo: mapped }];
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
model: {
|
|
218
|
+
supported: true,
|
|
219
|
+
values: null,
|
|
220
|
+
examples,
|
|
221
|
+
default: findFlagValue(args, ['--model']) ?? models?.['default'] ?? null,
|
|
222
|
+
backendEffect: 'applied',
|
|
223
|
+
note: "A tier alias the gateway's env maps, or a model name the provider accepts, passed as --model.",
|
|
224
|
+
},
|
|
225
|
+
effort: {
|
|
226
|
+
supported: true,
|
|
227
|
+
values: CLAUDE_EFFORT_LEVELS,
|
|
228
|
+
examples: [],
|
|
229
|
+
default: findFlagValue(args, ['--effort']),
|
|
230
|
+
backendEffect: 'unverified',
|
|
231
|
+
note: "Passed as --effort; the gateway's provider may ignore it.",
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
@@ -15,10 +15,12 @@ import type {
|
|
|
15
15
|
AgentProfile,
|
|
16
16
|
NameUpdate,
|
|
17
17
|
ResumeCheck,
|
|
18
|
+
SpawnOptionSpec,
|
|
18
19
|
SpawnOptions,
|
|
19
20
|
SpawnPlan,
|
|
20
21
|
} from './agent-adapter';
|
|
21
22
|
import { normalizeHookEventName } from './normalize-hook-event';
|
|
23
|
+
import { planPastedLineInput } from './plan-pasted-line-input';
|
|
22
24
|
import { resolveAgentHome } from './resolve-agent-home';
|
|
23
25
|
import { truncateDetail } from './truncate-detail';
|
|
24
26
|
|
|
@@ -48,6 +50,16 @@ const GROK_HOOK_PAYLOAD_SCHEMA = z.object({
|
|
|
48
50
|
|
|
49
51
|
type GrokHookPayload = z.infer<typeof GROK_HOOK_PAYLOAD_SCHEMA>;
|
|
50
52
|
|
|
53
|
+
// atc passes Grok neither a model nor an effort; the CLI's own config picks both.
|
|
54
|
+
const GROK_UNSUPPORTED_OPTION: SpawnOptionSpec = {
|
|
55
|
+
supported: false,
|
|
56
|
+
values: null,
|
|
57
|
+
examples: [],
|
|
58
|
+
default: null,
|
|
59
|
+
backendEffect: null,
|
|
60
|
+
note: 'atc does not pass this option to Grok.',
|
|
61
|
+
};
|
|
62
|
+
|
|
51
63
|
/**
|
|
52
64
|
* The Grok Build adapter: spawn arguments, hook payload mapping,
|
|
53
65
|
* resume semantics, and summary.json name-pulling.
|
|
@@ -65,13 +77,24 @@ export class GrokAdapter implements AgentAdapter {
|
|
|
65
77
|
|
|
66
78
|
readonly takesMessages = false;
|
|
67
79
|
|
|
80
|
+
// Grok's TUI keeps a newline that arrives inside a burst of input, so a
|
|
81
|
+
// line is pasted and then submitted.
|
|
82
|
+
readonly planLineInput = planPastedLineInput;
|
|
83
|
+
|
|
68
84
|
private readonly config: Config;
|
|
69
85
|
|
|
70
86
|
private readonly hookState = new Map<SessionID, GrokSessionHookState>();
|
|
71
87
|
|
|
72
88
|
constructor(config: Config) {
|
|
73
89
|
this.config = config;
|
|
74
|
-
|
|
90
|
+
|
|
91
|
+
this.profile = {
|
|
92
|
+
label: 'Grok',
|
|
93
|
+
kind: 'grok',
|
|
94
|
+
bin: config.grokBin,
|
|
95
|
+
models: null,
|
|
96
|
+
spawnOptions: { model: GROK_UNSUPPORTED_OPTION, effort: GROK_UNSUPPORTED_OPTION },
|
|
97
|
+
};
|
|
75
98
|
}
|
|
76
99
|
|
|
77
100
|
planSpawn(opts: SpawnOptions): SpawnPlan {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { HeadlessRunEvents, HeadlessRunner } from './agent-adapter';
|
|
2
|
+
import type { ClaudeHeadlessRunRequest } from './build-claude-query-options';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runs one headless turn through the Claude CLI; the daemon command supplies
|
|
6
|
+
* the Agent SDK implementation, and tests supply a recorder.
|
|
7
|
+
*/
|
|
8
|
+
export type ClaudeHeadlessRun = (
|
|
9
|
+
opts: ClaudeHeadlessRunRequest,
|
|
10
|
+
hooks: HeadlessRunEvents,
|
|
11
|
+
) => { readonly stop: () => void };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Where and how a Claude CLI headless turn runs: the binary, the permission
|
|
15
|
+
* mode its adapter resolved, the mod folder, and, for a gateway, the
|
|
16
|
+
* settings file that carries its backend. The folder and file are written
|
|
17
|
+
* on first use.
|
|
18
|
+
*/
|
|
19
|
+
interface ClaudeHeadlessTarget {
|
|
20
|
+
readonly claudeBin: string;
|
|
21
|
+
readonly permissionMode: string;
|
|
22
|
+
readonly pluginDir: () => string;
|
|
23
|
+
readonly settings?: () => string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Turns the Claude CLI's headless run into the neutral runner the daemon
|
|
28
|
+
* calls. Every headless turn runs under the permission mode its adapter
|
|
29
|
+
* resolved for the agent's sessions.
|
|
30
|
+
*/
|
|
31
|
+
export function makeClaudeHeadlessRunner(
|
|
32
|
+
run: ClaudeHeadlessRun,
|
|
33
|
+
target: ClaudeHeadlessTarget,
|
|
34
|
+
): HeadlessRunner {
|
|
35
|
+
return (opts, hooks) =>
|
|
36
|
+
run(
|
|
37
|
+
{
|
|
38
|
+
...opts,
|
|
39
|
+
claudeBin: target.claudeBin,
|
|
40
|
+
permissionMode: target.permissionMode,
|
|
41
|
+
pluginDir: target.pluginDir(),
|
|
42
|
+
...(target.settings === undefined ? {} : { settings: target.settings() }),
|
|
43
|
+
},
|
|
44
|
+
hooks,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TerminalInputModes } from './agent-adapter';
|
|
2
|
+
|
|
3
|
+
const PASTE_START = '\u001B[200~';
|
|
4
|
+
const PASTE_END = '\u001B[201~';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Pastes a line into a TUI that tells a paste from typing, then submits it
|
|
8
|
+
* with a carriage return as its own write. Such a TUI takes a newline inside
|
|
9
|
+
* a burst of input as part of the text, so a line typed with its newline
|
|
10
|
+
* stays unsent in the composer. Between bracketed paste markers the whole
|
|
11
|
+
* text is one paste event, newlines included, and the carriage return after
|
|
12
|
+
* the closing marker is a separate Enter key event however the two writes
|
|
13
|
+
* arrive. Paste markers inside the text are dropped, so the text can neither
|
|
14
|
+
* end the paste early nor start one of its own. A TUI that has not turned
|
|
15
|
+
* bracketed paste on gets the text unmarked, and may still read both writes
|
|
16
|
+
* as one burst.
|
|
17
|
+
*/
|
|
18
|
+
export function planPastedLineInput(text: string, modes: TerminalInputModes): readonly string[] {
|
|
19
|
+
const unmarked = text.replaceAll(PASTE_START, '').replaceAll(PASTE_END, '');
|
|
20
|
+
|
|
21
|
+
return modes.bracketedPaste ? [`${PASTE_START}${unmarked}${PASTE_END}`, '\r'] : [unmarked, '\r'];
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types a line the way a terminal user does: the text and a newline in one
|
|
3
|
+
* write. A TUI that reads input a line at a time takes the newline as the
|
|
4
|
+
* submit key.
|
|
5
|
+
*/
|
|
6
|
+
export function planTypedLineInput(text: string): readonly string[] {
|
|
7
|
+
return [`${text}\n`];
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { findClaudePermissionMode } from './find-claude-permission-mode';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The permission mode a Claude CLI agent's headless turns run in: the mode
|
|
5
|
+
* its configuration sets, else auto, since no human is at a terminal to
|
|
6
|
+
* answer a prompt.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveClaudePermissionMode(
|
|
9
|
+
args: readonly string[],
|
|
10
|
+
settings: Readonly<Record<string, unknown>> | undefined,
|
|
11
|
+
): string {
|
|
12
|
+
return findClaudePermissionMode(args, settings) ?? 'auto';
|
|
13
|
+
}
|
|
@@ -1,31 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { socketPath } from '../shared/config';
|
|
1
|
+
import { truncateSummary } from '../daemon/truncate-summary';
|
|
3
2
|
import { isCompiledBinary } from '../shared/is-compiled-binary';
|
|
4
3
|
import { isRecord } from '../shared/report';
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import { resolveHeadlessExecutable } from './resolve-headless-executable';
|
|
8
|
-
import { truncateSummary } from './truncate-summary';
|
|
9
|
-
|
|
10
|
-
const PERMISSION_MODES = [
|
|
11
|
-
'default',
|
|
12
|
-
'acceptEdits',
|
|
13
|
-
'bypassPermissions',
|
|
14
|
-
'plan',
|
|
15
|
-
'auto',
|
|
16
|
-
'dontAsk',
|
|
17
|
-
] as const;
|
|
18
|
-
|
|
19
|
-
interface HeadlessRunOptions {
|
|
20
|
-
readonly claudeBin: string;
|
|
21
|
-
readonly cwd: string;
|
|
22
|
-
readonly prompt: string;
|
|
23
|
-
readonly resume?: AgentSessionID;
|
|
24
|
-
readonly permissionMode?: string;
|
|
25
|
-
readonly settings?: string;
|
|
26
|
-
readonly sessionID?: SessionID;
|
|
27
|
-
readonly pluginDir?: string;
|
|
28
|
-
}
|
|
4
|
+
import { buildClaudeQueryOptions } from './build-claude-query-options';
|
|
5
|
+
import type { ClaudeHeadlessRunRequest } from './build-claude-query-options';
|
|
29
6
|
|
|
30
7
|
interface HeadlessRunHooks {
|
|
31
8
|
readonly onOutput: (text: string) => void;
|
|
@@ -43,15 +20,14 @@ interface HeadlessRunHandle {
|
|
|
43
20
|
* done (with the turn's whole result) or needs_you (errors, turn limits,
|
|
44
21
|
* anything a human must look at).
|
|
45
22
|
*/
|
|
46
|
-
export function
|
|
47
|
-
opts:
|
|
23
|
+
export function startClaudeHeadlessRun(
|
|
24
|
+
opts: ClaudeHeadlessRunRequest,
|
|
48
25
|
hooks: HeadlessRunHooks,
|
|
49
26
|
): HeadlessRunHandle {
|
|
50
27
|
const controller = new AbortController();
|
|
51
28
|
|
|
52
29
|
void (async () => {
|
|
53
30
|
try {
|
|
54
|
-
const mode = PERMISSION_MODES.find((m) => m === opts.permissionMode);
|
|
55
31
|
let stderrTail = '';
|
|
56
32
|
|
|
57
33
|
// The SDK is the heaviest module in the daemon's graph, and only a
|
|
@@ -62,23 +38,11 @@ export function startHeadlessRun(
|
|
|
62
38
|
const stream = sdk.query({
|
|
63
39
|
prompt: opts.prompt,
|
|
64
40
|
options: {
|
|
65
|
-
|
|
66
|
-
env: buildHeadlessEnv({
|
|
67
|
-
socketPath,
|
|
68
|
-
...(opts.pluginDir === undefined ? {} : { pluginDir: opts.pluginDir }),
|
|
69
|
-
...(opts.sessionID === undefined ? {} : { sessionID: opts.sessionID }),
|
|
70
|
-
}),
|
|
41
|
+
...buildClaudeQueryOptions(opts, isCompiledBinary()),
|
|
71
42
|
abortController: controller,
|
|
72
43
|
stderr: (data: string) => {
|
|
73
44
|
stderrTail = `${stderrTail}${data}`.slice(-2000);
|
|
74
45
|
},
|
|
75
|
-
...(opts.resume === undefined ? {} : { resume: opts.resume }),
|
|
76
|
-
...(mode === undefined ? {} : { permissionMode: mode }),
|
|
77
|
-
|
|
78
|
-
// The same generated file the terminal spawn passes, so the turn
|
|
79
|
-
// runs against the session's own backend and instrumentation.
|
|
80
|
-
...(opts.settings === undefined ? {} : { extraArgs: { settings: opts.settings } }),
|
|
81
|
-
...resolveHeadlessExecutable(opts.claudeBin, isCompiledBinary()),
|
|
82
46
|
},
|
|
83
47
|
});
|
|
84
48
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { stateDir } from '../shared/config';
|
|
4
|
-
import {
|
|
4
|
+
import { buildATCBridgeFiles } from './build-atc-bridge-files';
|
|
5
5
|
import { buildCLIArgv } from './build-cli-argv';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -13,7 +13,7 @@ import { buildCLIArgv } from './build-cli-argv';
|
|
|
13
13
|
* there.
|
|
14
14
|
*/
|
|
15
15
|
export function writeATCBridge(dir: string = join(stateDir, 'atc-bridge')): string {
|
|
16
|
-
const files =
|
|
16
|
+
const files = buildATCBridgeFiles(buildCLIArgv());
|
|
17
17
|
|
|
18
18
|
for (const [path, content] of Object.entries(files)) {
|
|
19
19
|
const file = join(dir, path);
|
|
@@ -29,10 +29,6 @@ export function writeATCBridge(dir: string = join(stateDir, 'atc-bridge')): stri
|
|
|
29
29
|
return dir;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function renderATCCLIModule(argv: readonly string[]): string {
|
|
33
|
-
return `export const ATC_CLI: readonly string[] = ${JSON.stringify(argv)};\n`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
32
|
function tryReadFileText(file: string): string | null {
|
|
37
33
|
try {
|
|
38
34
|
return readFileSync(file, 'utf8');
|
package/src/cli.ts
CHANGED
|
@@ -159,12 +159,28 @@ const main = defineCommand({
|
|
|
159
159
|
const grok = await import('./agents/grok-adapter');
|
|
160
160
|
const codex = await import('./agents/codex-adapter');
|
|
161
161
|
const gateway = await import('./agents/gateway-adapter');
|
|
162
|
-
const headless = await import('./
|
|
162
|
+
const headless = await import('./agents/start-claude-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();
|
|
169
|
+
const built = targets.buildExecutionTargets(cfg.targets);
|
|
170
|
+
const targetErrors = [...cfg.targetErrors, ...built.errors];
|
|
171
|
+
|
|
172
|
+
for (const error of targetErrors) {
|
|
173
|
+
const line =
|
|
174
|
+
error.scope === 'config'
|
|
175
|
+
? `${error.path} cannot be used (${error.problem}: ${error.detail}); every spawn is refused, local ones included, until it is fixed`
|
|
176
|
+
: error.problem;
|
|
177
|
+
|
|
178
|
+
console.error(`atc daemon: config: ${line}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const problem of cfg.principalErrors) {
|
|
182
|
+
console.error(`atc daemon: config: ${problem}`);
|
|
183
|
+
}
|
|
168
184
|
|
|
169
185
|
// Cap on how long a fleet restore waits for one revived session to
|
|
170
186
|
// report it has booted before moving to the next. Tests pin it to
|
|
@@ -178,18 +194,12 @@ const main = defineCommand({
|
|
|
178
194
|
// to it is refused. Tests pin it to 0 to reach the refusal at once.
|
|
179
195
|
const graceOverride = Number(process.env['ATC_TAP_GRACE_MS']);
|
|
180
196
|
|
|
181
|
-
const claudeAdapter = new claude.ClaudeAdapter(cfg,
|
|
182
|
-
headless.startHeadlessRun({ ...runOpts, claudeBin: cfg.claudeBin }, hooks),
|
|
183
|
-
);
|
|
184
|
-
|
|
197
|
+
const claudeAdapter = new claude.ClaudeAdapter(cfg, headless.startClaudeHeadlessRun);
|
|
185
198
|
const grokAdapter = new grok.GrokAdapter(cfg);
|
|
186
199
|
const codexAdapter = new codex.CodexAdapter(cfg);
|
|
187
200
|
|
|
188
201
|
const gatewayAdapters = cfg.gateways.map(
|
|
189
|
-
(entry) =>
|
|
190
|
-
new gateway.GatewayAdapter(entry, cfg, (runOpts, hooks) =>
|
|
191
|
-
headless.startHeadlessRun({ ...runOpts, claudeBin: entry.bin }, hooks),
|
|
192
|
-
),
|
|
202
|
+
(entry) => new gateway.GatewayAdapter(entry, cfg, headless.startClaudeHeadlessRun),
|
|
193
203
|
);
|
|
194
204
|
|
|
195
205
|
let handle: Awaited<ReturnType<typeof daemon.startDaemon>>;
|
|
@@ -206,6 +216,10 @@ const main = defineCommand({
|
|
|
206
216
|
legacyFleetPath: config.legacyFleetFile,
|
|
207
217
|
pidPath: config.daemonPidFile,
|
|
208
218
|
hooks: cfg.hooks,
|
|
219
|
+
targets: built.targets,
|
|
220
|
+
defaultTarget: cfg.defaultTarget,
|
|
221
|
+
targetErrors,
|
|
222
|
+
principals: cfg.principals,
|
|
209
223
|
restoreBootTimeoutMs,
|
|
210
224
|
...(Number.isFinite(graceOverride) && graceOverride >= 0
|
|
211
225
|
? { 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
|
|