@zgeoff/atc 2.24.0 → 2.25.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 +27 -2
- package/src/agents/gateway-adapter.ts +21 -3
- package/src/client/collect-target-picks.ts +5 -0
- package/src/client/spawn-picker.ts +15 -1
- package/src/daemon/broker-auth-host.ts +25 -0
- package/src/daemon/broker-authority-error.ts +2 -1
- package/src/daemon/build-agent-list.ts +16 -3
- package/src/daemon/build-scoped-context.ts +2 -0
- package/src/daemon/build-target-list.ts +5 -0
- package/src/daemon/daemon-connection.ts +52 -2
- package/src/daemon/daemon-context.ts +12 -0
- package/src/daemon/daemon.ts +58 -1
- package/src/daemon/execution-provider.ts +50 -2
- package/src/daemon/imp-client-port.ts +71 -23
- package/src/daemon/imp-harness.ts +299 -18
- package/src/daemon/imp-port.ts +23 -1
- package/src/daemon/imp-provider.ts +161 -40
- package/src/daemon/local-pty-provider.ts +11 -0
- package/src/daemon/restore-fleet.ts +15 -1
- package/src/daemon/runtime-auth-binder.ts +1086 -0
- package/src/daemon/sessions.ts +565 -25
- package/src/daemon/verify-broker-authority.ts +10 -5
- package/src/mcp/mcp-tools.ts +2 -0
- package/src/mcp/require-daemon-features.ts +1 -0
- package/src/protocol/daemon-features.ts +4 -0
- package/src/protocol/protocol.ts +16 -0
- package/src/protocol/request-param-schemas.ts +5 -0
- package/src/shared/collect-gateways.ts +1 -22
- package/src/shared/is-broker-variable.ts +24 -0
- package/src/store/state-store.ts +11 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DaemonError } from '../protocol/daemon-error';
|
|
2
2
|
import { LineDecoder } from '../protocol/line-decoder';
|
|
3
3
|
import { isCompiledBinary } from '../shared/is-compiled-binary';
|
|
4
|
+
import type { BrokerAuthHost } from './broker-auth-host';
|
|
4
5
|
import { buildImpName } from './build-imp-name';
|
|
5
6
|
import { buildTarArchive } from './build-tar-archive';
|
|
6
7
|
import type {
|
|
@@ -96,6 +97,8 @@ export class ImpProvider implements ExecutionProvider {
|
|
|
96
97
|
// namespace an impd token's imp patterns are checked against.
|
|
97
98
|
readonly impPrefix: string;
|
|
98
99
|
|
|
100
|
+
readonly brokerAuth: BrokerAuthHost;
|
|
101
|
+
|
|
99
102
|
private readonly port: ImpPort;
|
|
100
103
|
|
|
101
104
|
private readonly target: ImpTargetOptions;
|
|
@@ -106,6 +109,14 @@ export class ImpProvider implements ExecutionProvider {
|
|
|
106
109
|
|
|
107
110
|
private readonly hosts = new Map<string, ImpHost>();
|
|
108
111
|
|
|
112
|
+
// Each imp's readyings, sleeps, and lease returns run one after another,
|
|
113
|
+
// in the order they were asked for, so none of them gives back the lease
|
|
114
|
+
// a readying just took.
|
|
115
|
+
private readonly turns = new Map<string, Promise<void>>();
|
|
116
|
+
|
|
117
|
+
// The readyings of each imp still waiting for their turn.
|
|
118
|
+
private readonly waitingPrepares = new Map<string, number>();
|
|
119
|
+
|
|
109
120
|
private readonly atcBinary: string | null;
|
|
110
121
|
|
|
111
122
|
// Whether impd carries output offsets, once a prepare has read its
|
|
@@ -132,6 +143,19 @@ export class ImpProvider implements ExecutionProvider {
|
|
|
132
143
|
dir,
|
|
133
144
|
atc: target.guestATC ?? (this.atcBinary === null ? null : `${dir}/bin/atc`),
|
|
134
145
|
};
|
|
146
|
+
|
|
147
|
+
this.brokerAuth = {
|
|
148
|
+
impPrefix: this.impPrefix,
|
|
149
|
+
port,
|
|
150
|
+
getImpName: (hostKey) => this.getImpName(hostKey),
|
|
151
|
+
createImp: (hostKey) =>
|
|
152
|
+
port.createImp({
|
|
153
|
+
name: this.getImpName(hostKey),
|
|
154
|
+
...(target.image === undefined ? {} : { image: target.image }),
|
|
155
|
+
...(target.memoryMib === undefined ? {} : { memoryMib: target.memoryMib }),
|
|
156
|
+
}),
|
|
157
|
+
destroyImp: (hostKey) => this.destroyHost(hostKey),
|
|
158
|
+
};
|
|
135
159
|
}
|
|
136
160
|
|
|
137
161
|
// The imp a host key runs on, under this provider's prefix.
|
|
@@ -141,7 +165,25 @@ export class ImpProvider implements ExecutionProvider {
|
|
|
141
165
|
|
|
142
166
|
// Creates the host's imp when impd holds none, then takes the daemon's
|
|
143
167
|
// lease, which boots or wakes the imp.
|
|
144
|
-
readonly prepareHost =
|
|
168
|
+
readonly prepareHost = (request: HostRequest): Promise<void> => {
|
|
169
|
+
const name = this.getImpName(request.host);
|
|
170
|
+
|
|
171
|
+
this.waitingPrepares.set(name, (this.waitingPrepares.get(name) ?? 0) + 1);
|
|
172
|
+
|
|
173
|
+
return this.withHostTurn(name, () => {
|
|
174
|
+
const left = (this.waitingPrepares.get(name) ?? 1) - 1;
|
|
175
|
+
|
|
176
|
+
if (left === 0) {
|
|
177
|
+
this.waitingPrepares.delete(name);
|
|
178
|
+
} else {
|
|
179
|
+
this.waitingPrepares.set(name, left);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return this.prepareHostInTurn(request);
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
private readonly prepareHostInTurn = async (request: HostRequest): Promise<void> => {
|
|
145
187
|
const name = this.getImpName(request.host);
|
|
146
188
|
const label = `atc-${request.daemonID}`;
|
|
147
189
|
|
|
@@ -187,9 +229,11 @@ export class ImpProvider implements ExecutionProvider {
|
|
|
187
229
|
harnesses: 0,
|
|
188
230
|
renewTimer: null,
|
|
189
231
|
suspending: false,
|
|
232
|
+
isIdle: null,
|
|
190
233
|
};
|
|
191
234
|
|
|
192
235
|
host.suspending = false;
|
|
236
|
+
host.isIdle = request.isIdle ?? null;
|
|
193
237
|
|
|
194
238
|
this.hosts.set(request.host, host);
|
|
195
239
|
this.startRenewal(host, label);
|
|
@@ -201,6 +245,7 @@ export class ImpProvider implements ExecutionProvider {
|
|
|
201
245
|
harnesses: 0,
|
|
202
246
|
renewTimer: null,
|
|
203
247
|
suspending: false,
|
|
248
|
+
isIdle: null,
|
|
204
249
|
};
|
|
205
250
|
|
|
206
251
|
this.hosts.set(spec.host, host);
|
|
@@ -225,11 +270,13 @@ export class ImpProvider implements ExecutionProvider {
|
|
|
225
270
|
cwd: spec.cwd,
|
|
226
271
|
cols: spec.cols,
|
|
227
272
|
rows: spec.rows,
|
|
273
|
+
...(spec.requireBroker === true ? { require: ['broker'] } : {}),
|
|
228
274
|
},
|
|
229
275
|
{
|
|
230
276
|
offsets: this.offsets,
|
|
231
277
|
reconnectDelaysMs: this.reconnectDelaysMs,
|
|
232
278
|
...(reporter === null ? {} : { ready: reporter.forward.listening }),
|
|
279
|
+
...(spec.admit === undefined ? {} : { admit: spec.admit }),
|
|
233
280
|
isSuspending: () => host.suspending,
|
|
234
281
|
onDone: () => {
|
|
235
282
|
reporter?.forward.stop();
|
|
@@ -271,37 +318,57 @@ export class ImpProvider implements ExecutionProvider {
|
|
|
271
318
|
// then asks impd to sleep the imp without force. Another owner's lease
|
|
272
319
|
// refuses the sleep: the daemon takes its lease back and the refusal
|
|
273
320
|
// carries what impd showed of the other leases.
|
|
274
|
-
readonly suspendHost =
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
321
|
+
readonly suspendHost = (hostKey: string, isIdle?: () => boolean): Promise<void> =>
|
|
322
|
+
this.withHostTurn(this.getImpName(hostKey), async () => {
|
|
323
|
+
const host = this.hosts.get(hostKey) ?? {
|
|
324
|
+
name: this.getImpName(hostKey),
|
|
325
|
+
harnesses: 0,
|
|
326
|
+
renewTimer: null,
|
|
327
|
+
suspending: false,
|
|
328
|
+
isIdle: null,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
this.hosts.set(hostKey, host);
|
|
332
|
+
|
|
333
|
+
// A host a harness runs on, or that a readying waits for, is busy.
|
|
334
|
+
const isBusy = () =>
|
|
335
|
+
isIdle !== undefined &&
|
|
336
|
+
(!isIdle() || host.harnesses > 0 || (this.waitingPrepares.get(host.name) ?? 0) > 0);
|
|
337
|
+
|
|
338
|
+
if (isBusy()) {
|
|
339
|
+
throw buildBusyRefusal(host.name);
|
|
340
|
+
}
|
|
281
341
|
|
|
282
|
-
|
|
283
|
-
this.stopRenewal(host);
|
|
342
|
+
this.stopRenewal(host);
|
|
284
343
|
|
|
285
|
-
|
|
344
|
+
host.suspending = true;
|
|
286
345
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
346
|
+
try {
|
|
347
|
+
if (this.label !== null) {
|
|
348
|
+
await this.port.releaseLease(host.name, this.label);
|
|
349
|
+
}
|
|
291
350
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
host.suspending = false;
|
|
351
|
+
if (isBusy()) {
|
|
352
|
+
host.suspending = false;
|
|
295
353
|
|
|
296
|
-
|
|
297
|
-
await this.restoreLease(host);
|
|
354
|
+
await this.restoreLease(host);
|
|
298
355
|
|
|
299
|
-
|
|
300
|
-
|
|
356
|
+
throw buildBusyRefusal(host.name);
|
|
357
|
+
}
|
|
301
358
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
359
|
+
await this.port.suspendImp(host.name);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
host.suspending = false;
|
|
362
|
+
|
|
363
|
+
if (error instanceof ImpPortError && error.code === 'LEASED') {
|
|
364
|
+
await this.restoreLease(host);
|
|
365
|
+
|
|
366
|
+
throw buildLeasedRefusal(host.name, error.data);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
throw toHostRefusal(error, host.name);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
305
372
|
|
|
306
373
|
// A destroy ends every lease on the imp with it.
|
|
307
374
|
readonly destroyHost = async (hostKey: string): Promise<void> => {
|
|
@@ -437,13 +504,7 @@ export class ImpProvider implements ExecutionProvider {
|
|
|
437
504
|
|
|
438
505
|
host.renewTimer = setInterval(
|
|
439
506
|
() => {
|
|
440
|
-
|
|
441
|
-
void this.tryReleaseLease(host);
|
|
442
|
-
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
void this.tryRenewLease(host, label);
|
|
507
|
+
void this.refreshLease(host, label);
|
|
447
508
|
},
|
|
448
509
|
(this.leaseSeconds * 1000) / 3,
|
|
449
510
|
);
|
|
@@ -473,6 +534,28 @@ export class ImpProvider implements ExecutionProvider {
|
|
|
473
534
|
}
|
|
474
535
|
}
|
|
475
536
|
|
|
537
|
+
// Runs a readying, a sleep, or a lease return of an imp after the one
|
|
538
|
+
// before it there. A turn never starts in its caller's tick, so a harness
|
|
539
|
+
// end the daemon still applies counts before the turn checks the host.
|
|
540
|
+
private async withHostTurn<T>(name: string, run: () => Promise<T>): Promise<T> {
|
|
541
|
+
const before = this.turns.get(name) ?? Promise.resolve();
|
|
542
|
+
const turn = Promise.withResolvers<void>();
|
|
543
|
+
|
|
544
|
+
this.turns.set(name, turn.promise);
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
await before;
|
|
548
|
+
|
|
549
|
+
return await run();
|
|
550
|
+
} finally {
|
|
551
|
+
turn.resolve();
|
|
552
|
+
|
|
553
|
+
if (this.turns.get(name) === turn.promise) {
|
|
554
|
+
this.turns.delete(name);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
476
559
|
private async restoreLease(host: ImpHost): Promise<void> {
|
|
477
560
|
if (this.label === null) {
|
|
478
561
|
return;
|
|
@@ -487,19 +570,45 @@ export class ImpProvider implements ExecutionProvider {
|
|
|
487
570
|
this.startRenewal(host, this.label);
|
|
488
571
|
}
|
|
489
572
|
|
|
490
|
-
|
|
491
|
-
|
|
573
|
+
// A host with no harness gives its lease back, unless a launch readies it,
|
|
574
|
+
// which keeps the lease renewed.
|
|
575
|
+
private async refreshLease(host: ImpHost, label: string): Promise<void> {
|
|
576
|
+
if (host.harnesses === 0) {
|
|
577
|
+
const released = await this.tryReleaseLease(host);
|
|
492
578
|
|
|
493
|
-
|
|
494
|
-
|
|
579
|
+
if (released) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
495
582
|
}
|
|
496
583
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
} catch {
|
|
500
|
-
return false;
|
|
584
|
+
if (host.renewTimer !== null) {
|
|
585
|
+
await this.tryRenewLease(host, label);
|
|
501
586
|
}
|
|
502
587
|
}
|
|
588
|
+
|
|
589
|
+
// Gives an imp's lease back once nothing runs or starts there, in the
|
|
590
|
+
// imp's turn, so it never drops a lease a readying took for a launch.
|
|
591
|
+
private tryReleaseLease(host: ImpHost): Promise<boolean> {
|
|
592
|
+
return this.withHostTurn(host.name, async () => {
|
|
593
|
+
const isBusy =
|
|
594
|
+
host.harnesses > 0 ||
|
|
595
|
+
host.suspending ||
|
|
596
|
+
(this.waitingPrepares.get(host.name) ?? 0) > 0 ||
|
|
597
|
+
host.isIdle?.() === false;
|
|
598
|
+
|
|
599
|
+
if (isBusy || this.label === null) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
this.stopRenewal(host);
|
|
604
|
+
|
|
605
|
+
try {
|
|
606
|
+
return await this.port.releaseLease(host.name, this.label);
|
|
607
|
+
} catch {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|
|
503
612
|
}
|
|
504
613
|
|
|
505
614
|
// What the provider follows of one imp: its name, how many harnesses run
|
|
@@ -510,6 +619,10 @@ interface ImpHost {
|
|
|
510
619
|
harnesses: number;
|
|
511
620
|
renewTimer: ReturnType<typeof setInterval> | null;
|
|
512
621
|
suspending: boolean;
|
|
622
|
+
|
|
623
|
+
// The daemon's check that nothing runs or starts on the host, from its
|
|
624
|
+
// latest readying; null until one gives it.
|
|
625
|
+
isIdle: (() => boolean) | null;
|
|
513
626
|
}
|
|
514
627
|
|
|
515
628
|
function buildImpSessionName(sessionID: string): string {
|
|
@@ -571,6 +684,14 @@ function buildGuestEnv(env: Readonly<Record<string, string>>): Record<string, st
|
|
|
571
684
|
return { TERM: 'xterm-256color', LANG: 'C.UTF-8', PATH: GUEST_PATH, ...own };
|
|
572
685
|
}
|
|
573
686
|
|
|
687
|
+
function buildBusyRefusal(name: string): DaemonError {
|
|
688
|
+
return new DaemonError(
|
|
689
|
+
'host_unavailable',
|
|
690
|
+
`imp ${name} stays awake: a session runs there or is starting there`,
|
|
691
|
+
{ provider: 'imp', problem: 'busy' },
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
574
695
|
function buildLeasedRefusal(name: string, data: unknown): DaemonError {
|
|
575
696
|
const record = typeof data === 'object' && data !== null ? data : {};
|
|
576
697
|
const leases = 'leases' in record && Array.isArray(record.leases) ? record.leases : [];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir } from 'node:fs/promises';
|
|
2
2
|
import { spawn } from 'bun-pty';
|
|
3
|
+
import { DaemonError } from '../protocol/daemon-error';
|
|
3
4
|
import { collectCleanEnv } from '../shared/collect-clean-env';
|
|
4
5
|
import type {
|
|
5
6
|
CommandResult,
|
|
@@ -39,7 +40,17 @@ export class LocalPTYProvider implements ExecutionProvider {
|
|
|
39
40
|
|
|
40
41
|
// The daemon's machine keeps no process the daemon lets go of, so a
|
|
41
42
|
// detach ends the harness as a kill does, after dropping every listener.
|
|
43
|
+
// It has no credential broker, so a harness that requires one never
|
|
44
|
+
// starts here.
|
|
42
45
|
readonly spawnHarness = (spec: HarnessSpec): HarnessHandle => {
|
|
46
|
+
if (spec.requireBroker === true) {
|
|
47
|
+
throw new DaemonError(
|
|
48
|
+
'auth_target_unsupported',
|
|
49
|
+
"the daemon's own machine has no credential broker to start the harness behind",
|
|
50
|
+
{ provider: 'local-pty' },
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
const pty = spawn(spec.bin, [...spec.args], {
|
|
44
55
|
name: 'xterm-256color',
|
|
45
56
|
cols: spec.cols,
|
|
@@ -185,7 +185,8 @@ function isSameSession(s: Session, entry: FleetEntry): boolean {
|
|
|
185
185
|
|
|
186
186
|
// The refusals that leave one session without a terminal: its target is
|
|
187
187
|
// misconfigured, gone from the config, changed, has no provider here, or
|
|
188
|
-
// cannot start a terminal, or its agent refuses every start
|
|
188
|
+
// cannot start a terminal, or its agent refuses every start, or its
|
|
189
|
+
// host's runtime auth binding refuses the launch, its broker included.
|
|
189
190
|
const REFUSED_ADOPT_CODES: ReadonlySet<string> = new Set([
|
|
190
191
|
'unsupported_operation',
|
|
191
192
|
'unknown_target',
|
|
@@ -195,6 +196,19 @@ const REFUSED_ADOPT_CODES: ReadonlySet<string> = new Set([
|
|
|
195
196
|
'host_unavailable',
|
|
196
197
|
'auth_not_configured',
|
|
197
198
|
'auth_target_unsupported',
|
|
199
|
+
'auth_impd_too_old',
|
|
200
|
+
'auth_token_scope',
|
|
201
|
+
'auth_token_too_broad',
|
|
202
|
+
'auth_imp_out_of_scope',
|
|
203
|
+
'auth_secret_not_grantable',
|
|
204
|
+
'auth_secret_mismatch',
|
|
205
|
+
'auth_runtime_mismatch',
|
|
206
|
+
'auth_grant_missing',
|
|
207
|
+
'auth_grants_mismatch',
|
|
208
|
+
'auth_rebind_required',
|
|
209
|
+
'auth_blocked',
|
|
210
|
+
'auth_binding_invalid',
|
|
211
|
+
'broker_not_ready',
|
|
198
212
|
]);
|
|
199
213
|
|
|
200
214
|
async function tryAdoptTerminal(
|