@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
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { createImpClient, openExecSession, openReverseForward } from '@zgeoff/imp-client';
|
|
2
|
+
import type { ExecOutcome, ImpClient } from '@zgeoff/imp-client';
|
|
3
|
+
import type {
|
|
4
|
+
ImpCommand,
|
|
5
|
+
ImpCommandResult,
|
|
6
|
+
ImpCreateSpec,
|
|
7
|
+
ImpFeatures,
|
|
8
|
+
ImpLease,
|
|
9
|
+
ImpPort,
|
|
10
|
+
ImpRelayConnection,
|
|
11
|
+
ImpReverseForward,
|
|
12
|
+
ImpSessionConnection,
|
|
13
|
+
ImpSessionHandlers,
|
|
14
|
+
ImpSessionOutcome,
|
|
15
|
+
ImpSessionRequest,
|
|
16
|
+
ImpView,
|
|
17
|
+
} from './imp-port';
|
|
18
|
+
import { ImpPortError } from './imp-port-error';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Where impd listens and the token the daemon calls it with. The token
|
|
22
|
+
* comes from the daemon's environment, never from a config file.
|
|
23
|
+
*/
|
|
24
|
+
export interface ImpClientPortOptions {
|
|
25
|
+
readonly url: string;
|
|
26
|
+
readonly token: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The imp port over `@zgeoff/imp-client`: RPC calls for imps and leases,
|
|
31
|
+
* one exec WebSocket per session connection, and one reverse forward per
|
|
32
|
+
* guest socket. Every refusal impd sends becomes an `ImpPortError` with its
|
|
33
|
+
* code and data, and a call that never reaches impd carries `UNREACHABLE`.
|
|
34
|
+
*/
|
|
35
|
+
export class ImpClientPort implements ImpPort {
|
|
36
|
+
private readonly url: string;
|
|
37
|
+
|
|
38
|
+
private readonly token: string | null;
|
|
39
|
+
|
|
40
|
+
private readonly client: ImpClient;
|
|
41
|
+
|
|
42
|
+
constructor(options: ImpClientPortOptions) {
|
|
43
|
+
this.url = options.url;
|
|
44
|
+
this.token = options.token;
|
|
45
|
+
|
|
46
|
+
this.client = createImpClient({
|
|
47
|
+
url: options.url,
|
|
48
|
+
...(options.token === null ? {} : { token: options.token }),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// An impd from before the flags has neither.
|
|
53
|
+
readonly readFeatures = async (): Promise<ImpFeatures> => {
|
|
54
|
+
const info = await this.tryCall(() => this.client.system.info());
|
|
55
|
+
|
|
56
|
+
return info.features ?? { sessionOffsets: false, leases: false };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
readonly readImp = async (name: string): Promise<ImpView | null> => {
|
|
60
|
+
try {
|
|
61
|
+
const imp = await this.tryCall(() => this.client.imps.get({ name }));
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
name: imp.name,
|
|
65
|
+
state: imp.state,
|
|
66
|
+
leases: (imp.leases?.leases ?? []).map((lease) => toLease(lease)),
|
|
67
|
+
otherLeaseCount: imp.leases?.otherCount ?? 0,
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error instanceof ImpPortError && error.code === 'NOT_FOUND') {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
readonly createImp = async (spec: ImpCreateSpec): Promise<ImpView> => {
|
|
79
|
+
const imp = await this.tryCall(() =>
|
|
80
|
+
this.client.imps.create({
|
|
81
|
+
name: spec.name,
|
|
82
|
+
...(spec.image === undefined ? {} : { image: spec.image }),
|
|
83
|
+
...(spec.memoryMib === undefined ? {} : { memoryMib: spec.memoryMib }),
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return { name: imp.name, state: imp.state, leases: [], otherLeaseCount: 0 };
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
readonly acquireLease = async (
|
|
91
|
+
name: string,
|
|
92
|
+
label: string,
|
|
93
|
+
ttlSeconds: number,
|
|
94
|
+
): Promise<ImpLease> => {
|
|
95
|
+
const lease = await this.tryCall(() => this.client.leases.acquire({ name, label, ttlSeconds }));
|
|
96
|
+
|
|
97
|
+
return toLease(lease);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
readonly renewLease = async (
|
|
101
|
+
name: string,
|
|
102
|
+
label: string,
|
|
103
|
+
ttlSeconds: number,
|
|
104
|
+
): Promise<ImpLease> => {
|
|
105
|
+
const lease = await this.tryCall(() => this.client.leases.renew({ name, label, ttlSeconds }));
|
|
106
|
+
|
|
107
|
+
return toLease(lease);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
readonly releaseLease = async (name: string, label: string): Promise<boolean> => {
|
|
111
|
+
const result = await this.tryCall(() => this.client.leases.release({ name, label }));
|
|
112
|
+
|
|
113
|
+
return result.released;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
readonly suspendImp = async (name: string): Promise<void> => {
|
|
117
|
+
await this.tryCall(() => this.client.imps.sleep({ name }));
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
readonly destroyImp = async (name: string): Promise<void> => {
|
|
121
|
+
await this.tryCall(() => this.client.imps.destroy({ name }));
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// The exec WebSocket answers only after it opens, so nothing reaches the
|
|
125
|
+
// handlers before this returns.
|
|
126
|
+
readonly openSession = (
|
|
127
|
+
request: ImpSessionRequest,
|
|
128
|
+
handlers: ImpSessionHandlers,
|
|
129
|
+
): ImpSessionConnection => {
|
|
130
|
+
const session = openExecSession({
|
|
131
|
+
baseUrl: this.url,
|
|
132
|
+
token: this.token,
|
|
133
|
+
start:
|
|
134
|
+
request.kind === 'start'
|
|
135
|
+
? {
|
|
136
|
+
name: request.name,
|
|
137
|
+
session: request.session,
|
|
138
|
+
argv: request.argv,
|
|
139
|
+
tty: true,
|
|
140
|
+
env: request.env,
|
|
141
|
+
cwd: request.cwd,
|
|
142
|
+
cols: request.cols,
|
|
143
|
+
rows: request.rows,
|
|
144
|
+
...(request.resumeFrom === undefined ? {} : { resumeFrom: request.resumeFrom }),
|
|
145
|
+
}
|
|
146
|
+
: {
|
|
147
|
+
name: request.name,
|
|
148
|
+
session: request.session,
|
|
149
|
+
cols: request.cols,
|
|
150
|
+
rows: request.rows,
|
|
151
|
+
wake: request.wake,
|
|
152
|
+
...(request.resumeFrom === undefined ? {} : { resumeFrom: request.resumeFrom }),
|
|
153
|
+
},
|
|
154
|
+
onStarted: (started) => {
|
|
155
|
+
handlers.onStarted({ created: started.created, output: started.output });
|
|
156
|
+
},
|
|
157
|
+
onOutput: (_channel, data) => {
|
|
158
|
+
handlers.onOutput(data);
|
|
159
|
+
},
|
|
160
|
+
connect: (url, headers) => new WebSocket(url, { headers }),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
outcome: waitForSessionOutcome(session.outcome),
|
|
165
|
+
write: (data) => {
|
|
166
|
+
session.sendStdin(data);
|
|
167
|
+
},
|
|
168
|
+
resize: (cols, rows) => {
|
|
169
|
+
session.resize(cols, rows);
|
|
170
|
+
},
|
|
171
|
+
sendSignal: (signal) => {
|
|
172
|
+
session.sendSignal(signal);
|
|
173
|
+
},
|
|
174
|
+
close: () => {
|
|
175
|
+
session.stop();
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- the command's input bytes have no readonly form
|
|
181
|
+
readonly runCommand = async (name: string, command: ImpCommand): Promise<ImpCommandResult> => {
|
|
182
|
+
const result = await this.tryCall(() =>
|
|
183
|
+
this.client.run(name, command.argv, {
|
|
184
|
+
...(command.cwd === undefined ? {} : { cwd: command.cwd }),
|
|
185
|
+
...(command.stdin === undefined ? {} : { stdin: command.stdin }),
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return { code: result.code, stdout: result.stdout, stderr: result.stderr };
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
readonly openReverseForward = (
|
|
193
|
+
name: string,
|
|
194
|
+
guestPath: string,
|
|
195
|
+
onConnection: (connection: ImpRelayConnection) => void,
|
|
196
|
+
): ImpReverseForward => {
|
|
197
|
+
const forward = openReverseForward({
|
|
198
|
+
baseUrl: this.url,
|
|
199
|
+
token: this.token,
|
|
200
|
+
name,
|
|
201
|
+
guest: { network: 'unix', path: guestPath },
|
|
202
|
+
connect: (url, headers) => new WebSocket(url, { headers }),
|
|
203
|
+
onConnection: (accept) => {
|
|
204
|
+
onConnection(openRelay(accept));
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
listening: waitForListening(forward.listening),
|
|
210
|
+
stop: () => {
|
|
211
|
+
forward.stop();
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Runs one client call, turning impd's refusals and transport failures
|
|
217
|
+
// into port errors.
|
|
218
|
+
private async tryCall<T>(call: () => Promise<T>): Promise<T> {
|
|
219
|
+
try {
|
|
220
|
+
return await call();
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw toPortError(error);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// A lease as the client returns it.
|
|
228
|
+
interface ClientLease {
|
|
229
|
+
readonly name: string;
|
|
230
|
+
readonly owner: Readonly<{ principal: string; display: string; label: string }>;
|
|
231
|
+
readonly until: Date | null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- a lease's end is a Date
|
|
235
|
+
function toLease(lease: ClientLease): ImpLease {
|
|
236
|
+
return {
|
|
237
|
+
name: lease.name,
|
|
238
|
+
owner: {
|
|
239
|
+
principal: lease.owner.principal,
|
|
240
|
+
display: lease.owner.display,
|
|
241
|
+
label: lease.owner.label,
|
|
242
|
+
},
|
|
243
|
+
until: lease.until?.getTime() ?? null,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function toPortError(error: unknown): ImpPortError {
|
|
248
|
+
if (error instanceof ImpPortError) {
|
|
249
|
+
return error;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// impd's refusals arrive as errors carrying impd's code and data.
|
|
253
|
+
if (error instanceof Error && 'code' in error && typeof error.code === 'string') {
|
|
254
|
+
const data: unknown = 'data' in error ? error.data : undefined;
|
|
255
|
+
|
|
256
|
+
return new ImpPortError(error.code, error.message, data);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
260
|
+
|
|
261
|
+
return new ImpPortError('UNREACHABLE', message);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- a promise is a live handle
|
|
265
|
+
async function waitForSessionOutcome(outcome: Promise<ExecOutcome>): Promise<ImpSessionOutcome> {
|
|
266
|
+
const ended = await outcome;
|
|
267
|
+
|
|
268
|
+
if (ended.kind === 'local_error') {
|
|
269
|
+
return {
|
|
270
|
+
kind: 'local_error',
|
|
271
|
+
detail: ended.error instanceof Error ? ended.error.message : String(ended.error),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (ended.kind === 'unauthorized') {
|
|
276
|
+
return { kind: 'unauthorized' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return ended;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- a promise is a live handle
|
|
283
|
+
async function waitForListening(listening: Promise<unknown>): Promise<void> {
|
|
284
|
+
await listening;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// How the client hands over a guest connection: the daemon accepts it with
|
|
288
|
+
// its handlers and gets the relay back.
|
|
289
|
+
type RelayAccept = (
|
|
290
|
+
handlers: Readonly<{
|
|
291
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- relayed bytes have no readonly form
|
|
292
|
+
onData: (data: Uint8Array) => void;
|
|
293
|
+
onEof: () => void;
|
|
294
|
+
onClose: (lost: boolean) => void;
|
|
295
|
+
}>,
|
|
296
|
+
) => Readonly<{
|
|
297
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- relayed bytes have no readonly form
|
|
298
|
+
send: (data: Uint8Array) => boolean;
|
|
299
|
+
waitForRoom: () => Promise<void>;
|
|
300
|
+
close: () => void;
|
|
301
|
+
}>;
|
|
302
|
+
|
|
303
|
+
// Takes a guest connection at once, handing its bytes and its close to the
|
|
304
|
+
// listeners the relay's owner adds.
|
|
305
|
+
function openRelay(accept: RelayAccept): ImpRelayConnection {
|
|
306
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- relayed bytes have no readonly form
|
|
307
|
+
const dataListeners: ((data: Uint8Array) => void)[] = [];
|
|
308
|
+
const closeListeners: (() => void)[] = [];
|
|
309
|
+
|
|
310
|
+
const relay = accept({
|
|
311
|
+
onData: (data) => {
|
|
312
|
+
for (const listener of dataListeners) {
|
|
313
|
+
listener(data);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
onEof: () => {},
|
|
317
|
+
onClose: () => {
|
|
318
|
+
for (const listener of closeListeners) {
|
|
319
|
+
listener();
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
onData: (listener) => {
|
|
326
|
+
dataListeners.push(listener);
|
|
327
|
+
},
|
|
328
|
+
onClose: (listener) => {
|
|
329
|
+
closeListeners.push(listener);
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
// A send past the relay's window still queues the bytes, so only the
|
|
333
|
+
// next write waits; a closed relay takes bytes and drops them.
|
|
334
|
+
write: async (data) => {
|
|
335
|
+
if (!relay.send(data)) {
|
|
336
|
+
await relay.waitForRoom();
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
close: () => {
|
|
340
|
+
relay.close();
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|