@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.
Files changed (119) hide show
  1. package/README.md +10 -10
  2. package/package.json +2 -1
  3. package/src/agents/agent-adapter.ts +98 -10
  4. package/src/agents/build-args-without-flags.ts +25 -0
  5. package/src/agents/build-atc-bridge-files.ts +13 -0
  6. package/src/agents/build-claude-override-args.ts +26 -0
  7. package/src/agents/build-claude-query-options.ts +72 -0
  8. package/src/agents/build-cli-command.ts +7 -5
  9. package/src/{daemon → agents}/build-headless-env.ts +12 -6
  10. package/src/agents/build-hook-settings.ts +8 -2
  11. package/src/agents/build-restore-mode-args.ts +23 -0
  12. package/src/agents/claude-adapter.ts +112 -18
  13. package/src/agents/claude-effort-levels.ts +5 -0
  14. package/src/agents/codex-adapter.ts +48 -2
  15. package/src/agents/find-claude-permission-mode.ts +24 -0
  16. package/src/agents/find-flag-value.ts +27 -0
  17. package/src/agents/gateway-adapter.ts +83 -13
  18. package/src/agents/grok-adapter.ts +24 -1
  19. package/src/agents/make-claude-headless-runner.ts +46 -0
  20. package/src/agents/plan-pasted-line-input.ts +22 -0
  21. package/src/agents/plan-typed-line-input.ts +8 -0
  22. package/src/agents/resolve-claude-permission-mode.ts +13 -0
  23. package/src/{daemon/start-headless-run.ts → agents/start-claude-headless-run.ts} +6 -42
  24. package/src/agents/write-atc-bridge.ts +2 -6
  25. package/src/cli.ts +23 -9
  26. package/src/client/daemon-client.ts +14 -2
  27. package/src/client/index.ts +50 -1
  28. package/src/client/spawn-picker.ts +27 -4
  29. package/src/client/ui.ts +8 -0
  30. package/src/daemon/build-agent-list.ts +37 -1
  31. package/src/daemon/build-config-revision.ts +27 -0
  32. package/src/daemon/build-execution-targets.ts +73 -0
  33. package/src/daemon/build-fleet-events.ts +2 -1
  34. package/src/daemon/build-imp-provider.ts +68 -0
  35. package/src/daemon/build-payload-hash.ts +31 -0
  36. package/src/daemon/build-report-trail-entry.ts +4 -2
  37. package/src/daemon/build-scoped-context.ts +235 -0
  38. package/src/daemon/build-session-lifecycle.ts +52 -0
  39. package/src/daemon/build-tar-archive.ts +85 -0
  40. package/src/daemon/build-target-access.ts +33 -0
  41. package/src/daemon/build-target-forbidden-error.ts +13 -0
  42. package/src/daemon/build-target-identity.ts +22 -0
  43. package/src/daemon/build-target-list.ts +50 -0
  44. package/src/daemon/daemon-connection.ts +425 -96
  45. package/src/daemon/daemon.ts +730 -96
  46. package/src/daemon/effect-remains-error.ts +13 -0
  47. package/src/daemon/execution-provider.ts +208 -0
  48. package/src/daemon/find-execution-refusal.ts +104 -0
  49. package/src/daemon/hooks.ts +5 -17
  50. package/src/daemon/idempotency-ledger.ts +164 -0
  51. package/src/daemon/imp-client-port.ts +343 -0
  52. package/src/daemon/imp-harness.ts +618 -0
  53. package/src/daemon/imp-port-error.ts +18 -0
  54. package/src/daemon/imp-port.ts +246 -0
  55. package/src/daemon/imp-provider.ts +601 -0
  56. package/src/daemon/is-binding-current.ts +43 -0
  57. package/src/daemon/local-pty-provider.ts +142 -0
  58. package/src/daemon/materialize-workspace.ts +571 -0
  59. package/src/daemon/mint-session-id.ts +5 -5
  60. package/src/daemon/parse-hook-line.ts +31 -0
  61. package/src/daemon/parse-spawn-overrides.ts +94 -0
  62. package/src/daemon/permission-registry.ts +14 -4
  63. package/src/daemon/pick-session-state.ts +15 -0
  64. package/src/daemon/restore-fleet.ts +103 -38
  65. package/src/daemon/screen-model.ts +7 -0
  66. package/src/daemon/session-runtime.ts +10 -0
  67. package/src/daemon/sessions.ts +835 -105
  68. package/src/daemon/start-headless-turn.ts +12 -3
  69. package/src/daemon/start-session-bridge.ts +294 -0
  70. package/src/daemon/target-access.ts +36 -0
  71. package/src/mcp/answer-mcp-request.ts +12 -4
  72. package/src/mcp/answer-rpc-request.ts +28 -1
  73. package/src/mcp/build-principal-caller.ts +15 -0
  74. package/src/mcp/build-spawn-descriptions.ts +45 -0
  75. package/src/mcp/build-tool-list.ts +32 -4
  76. package/src/mcp/mcp-tools.ts +107 -10
  77. package/src/mcp/parse-idempotency-key.ts +29 -0
  78. package/src/mcp/reconnecting-caller.ts +39 -7
  79. package/src/mcp/require-daemon-features.ts +10 -0
  80. package/src/mcp/run-tool.ts +87 -16
  81. package/src/mcp/types.ts +2 -0
  82. package/src/protocol/daemon-error.ts +5 -1
  83. package/src/protocol/daemon-features.ts +35 -0
  84. package/src/protocol/protocol.ts +67 -11
  85. package/src/protocol/request-param-schemas.ts +126 -10
  86. package/src/report.ts +37 -2
  87. package/src/run-bridge-tap.ts +241 -0
  88. package/src/shared/agent-session-id.ts +1 -1
  89. package/src/shared/collect-clean-env.ts +9 -1
  90. package/src/shared/collect-principals.ts +51 -0
  91. package/src/shared/collect-targets.ts +144 -0
  92. package/src/shared/config.ts +139 -14
  93. package/src/shared/daemon-id.ts +8 -0
  94. package/src/shared/format-json-kind.ts +21 -0
  95. package/src/shared/open-bridge-socket.ts +86 -0
  96. package/src/shared/send-bridge-request.ts +47 -0
  97. package/src/shared/sort-json-keys.ts +19 -0
  98. package/src/shared/to-daemon-id.ts +11 -0
  99. package/src/statusline.ts +34 -3
  100. package/src/store/fleet-entry.ts +61 -9
  101. package/src/store/idempotency-record.ts +48 -0
  102. package/src/store/message-owner.ts +1 -1
  103. package/src/store/run-migrations.ts +330 -5
  104. package/src/store/state-store.ts +607 -44
  105. package/src/store/trail-entry.ts +4 -0
  106. package/src/store/workspace-materialization.ts +70 -0
  107. package/src/tap.ts +11 -0
  108. package/src/workspace/check-url-credentials.ts +59 -0
  109. package/src/workspace/check-workspace-completeness.ts +90 -0
  110. package/src/workspace/create-workspace-clone.ts +251 -0
  111. package/src/workspace/normalize-git-url.ts +59 -0
  112. package/src/workspace/read-workspace-tar.ts +38 -0
  113. package/src/workspace/repository-env-vars.ts +22 -0
  114. package/src/workspace/resolve-path-source.ts +170 -0
  115. package/src/workspace/run-git.ts +87 -0
  116. package/src/workspace/sanitize-workspace-clone.ts +146 -0
  117. package/src/workspace/workspace-provenance.ts +11 -0
  118. package/src/workspace/workspace-source.ts +8 -0
  119. /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
+ }