@zgeoff/atc 2.20.0 → 2.22.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/cli.ts +187 -118
- package/src/client/daemon-client.ts +29 -18
- package/src/daemon/build-payload-hash.ts +4 -3
- package/src/daemon/daemon-connection.ts +270 -20
- package/src/daemon/daemon-context.ts +8 -0
- package/src/daemon/daemon.ts +186 -32
- package/src/daemon/find-token-fingerprint.ts +23 -0
- package/src/daemon/handshake-throttle.ts +47 -0
- package/src/daemon/idempotency-ledger.ts +20 -2
- package/src/daemon/is-allowed-listen-host.ts +63 -0
- package/src/daemon/load-listener-tokens.ts +46 -0
- package/src/daemon/parse-listen-address.ts +33 -0
- package/src/daemon/start-tcp-listener.ts +165 -0
- package/src/federation/build-binding-payload-hash.ts +34 -0
- package/src/federation/build-daemon-outdated-error.ts +14 -0
- package/src/federation/build-events-filter-hash.ts +16 -0
- package/src/federation/build-gateway-error.ts +45 -0
- package/src/federation/build-gateway-id.ts +14 -0
- package/src/federation/build-gateway-result.ts +31 -0
- package/src/federation/build-ruled-value.ts +53 -0
- package/src/federation/collect-unruled-id-paths.ts +46 -0
- package/src/federation/daemon-caller.ts +473 -0
- package/src/federation/daemon-pool.ts +56 -0
- package/src/federation/decode-gateway-cursor.ts +73 -0
- package/src/federation/encode-gateway-cursor.ts +15 -0
- package/src/federation/gateway-error.ts +25 -0
- package/src/federation/gateway-store.ts +253 -0
- package/src/federation/id-rules.ts +85 -0
- package/src/federation/load-gateway-registry.ts +29 -0
- package/src/federation/max-events-cursor-bytes.ts +4 -0
- package/src/federation/max-registry-daemons.ts +26 -0
- package/src/federation/merge-event-pages.ts +228 -0
- package/src/federation/open-gateway-caller.ts +55 -0
- package/src/federation/parse-gateway-id.ts +28 -0
- package/src/federation/parse-gateway-registry.ts +123 -0
- package/src/federation/pick-daemon-state.ts +46 -0
- package/src/federation/plan-event-reads.ts +54 -0
- package/src/federation/read-fleet-events.ts +279 -0
- package/src/federation/require-serving-daemon.ts +27 -0
- package/src/federation/resolve-daemon-request.ts +59 -0
- package/src/federation/routing-caller.ts +450 -0
- package/src/federation/types.ts +33 -0
- package/src/federation/wait-for-outcome.ts +38 -0
- package/src/mcp/answer-rpc-request.ts +14 -1
- package/src/mcp/build-tool-list.ts +6 -5
- package/src/mcp/mcp-tools.ts +41 -8
- package/src/mcp/require-daemon-features.ts +2 -0
- package/src/mcp/run-tool.ts +15 -1
- package/src/mcp/start-mcp-http-server.ts +53 -9
- package/src/mcp/types.ts +8 -1
- package/src/protocol/daemon-features.ts +9 -0
- package/src/protocol/protocol.ts +1 -0
- package/src/protocol/request-param-schemas.ts +6 -0
- package/src/run-daemon-id.ts +52 -0
- package/src/shared/find-daemon-record.ts +8 -3
- package/src/store/state-store.ts +16 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -3,10 +3,23 @@
|
|
|
3
3
|
import { defineCommand, runMain } from 'citty';
|
|
4
4
|
import pkg from '../package.json';
|
|
5
5
|
import { collectRedirectURIs } from './collect-redirect-uris';
|
|
6
|
+
import { parseListenAddress } from './daemon/parse-listen-address';
|
|
6
7
|
import { normalizeCLIArgs } from './normalize-cli-args';
|
|
7
8
|
import { parsePort } from './parse-port';
|
|
8
9
|
import { getBuild } from './shared/get-build';
|
|
9
10
|
|
|
11
|
+
// The flags `atc daemon` takes.
|
|
12
|
+
const DAEMON_ARGS = {
|
|
13
|
+
listen: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Serve the client protocol on <host>:<port> too, a loopback or tailnet address',
|
|
16
|
+
},
|
|
17
|
+
'token-file': {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'File holding the one or two bearer tokens a --listen handshake must present',
|
|
20
|
+
},
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
10
23
|
const main = defineCommand({
|
|
11
24
|
meta: {
|
|
12
25
|
name: 'atc',
|
|
@@ -152,127 +165,36 @@ const main = defineCommand({
|
|
|
152
165
|
name: 'daemon',
|
|
153
166
|
description: 'Run the atc daemon in the foreground',
|
|
154
167
|
},
|
|
155
|
-
async run() {
|
|
156
|
-
const daemon = await import('./daemon/daemon');
|
|
157
|
-
const config = await import('./shared/config');
|
|
158
|
-
const claude = await import('./agents/claude-adapter');
|
|
159
|
-
const grok = await import('./agents/grok-adapter');
|
|
160
|
-
const codex = await import('./agents/codex-adapter');
|
|
161
|
-
const gateway = await import('./agents/gateway-adapter');
|
|
162
|
-
const headless = await import('./agents/start-claude-headless-run');
|
|
163
|
-
const targets = await import('./daemon/build-execution-targets');
|
|
164
|
-
const sourceOrder = await import('./sources/build-sources');
|
|
165
|
-
const builtinSources = await import('./sources/collect-builtin-sources');
|
|
166
|
-
const zoxide = await import('./shared/collect-zoxide-dirs');
|
|
167
|
-
const home = await import('./shared/resolve-home-dir');
|
|
168
|
-
|
|
169
|
-
// Test harnesses shrink the outbound queue to force overflow
|
|
170
|
-
// deterministically; unset means the production default.
|
|
171
|
-
const queueBytes = Number(process.env['ATC_QUEUE_BYTES']);
|
|
172
|
-
const cfg = config.loadConfig();
|
|
173
|
-
const built = targets.buildExecutionTargets(cfg.targets);
|
|
174
|
-
const targetErrors = [...cfg.targetErrors, ...built.errors];
|
|
175
|
-
|
|
176
|
-
for (const error of targetErrors) {
|
|
177
|
-
const line =
|
|
178
|
-
error.scope === 'config'
|
|
179
|
-
? `${error.path} cannot be used (${error.problem}: ${error.detail}); every spawn is refused, local ones included, until it is fixed`
|
|
180
|
-
: error.problem;
|
|
181
|
-
|
|
182
|
-
console.error(`atc daemon: config: ${line}`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
for (const problem of [
|
|
186
|
-
...cfg.principalErrors,
|
|
187
|
-
...cfg.workspaceErrors,
|
|
188
|
-
...cfg.authProfileErrors,
|
|
189
|
-
...cfg.gatewayErrors,
|
|
190
|
-
]) {
|
|
191
|
-
console.error(`atc daemon: config: ${problem}`);
|
|
192
|
-
}
|
|
193
168
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
169
|
+
// Declared here as well as on `serve`, so the parser skips their
|
|
170
|
+
// values when it looks for a subcommand name.
|
|
171
|
+
args: DAEMON_ARGS,
|
|
172
|
+
default: 'serve',
|
|
173
|
+
subCommands: {
|
|
174
|
+
serve: () =>
|
|
175
|
+
defineCommand({
|
|
176
|
+
meta: {
|
|
177
|
+
name: 'serve',
|
|
178
|
+
description: 'Run the atc daemon in the foreground',
|
|
179
|
+
hidden: true,
|
|
180
|
+
},
|
|
181
|
+
args: DAEMON_ARGS,
|
|
182
|
+
async run(ctx) {
|
|
183
|
+
await runDaemon(ctx.args.listen ?? null, ctx.args['token-file'] ?? null);
|
|
184
|
+
},
|
|
201
185
|
}),
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// Cap on how long a fleet restore waits for one revived session to
|
|
212
|
-
// report it has booted before moving to the next. Tests pin it to
|
|
213
|
-
// keep timing deterministic; unset means the production default.
|
|
214
|
-
const capOverride = Number(process.env['ATC_RESTORE_BOOT_TIMEOUT_MS']);
|
|
215
|
-
|
|
216
|
-
const restoreBootTimeoutMs =
|
|
217
|
-
Number.isFinite(capOverride) && capOverride >= 0 ? capOverride : 15_000;
|
|
218
|
-
|
|
219
|
-
// How long a started session may go without a tap before a message
|
|
220
|
-
// to it is refused. Tests pin it to 0 to reach the refusal at once.
|
|
221
|
-
const graceOverride = Number(process.env['ATC_TAP_GRACE_MS']);
|
|
222
|
-
|
|
223
|
-
const claudeAdapter = new claude.ClaudeAdapter(cfg, headless.startClaudeHeadlessRun);
|
|
224
|
-
const grokAdapter = new grok.GrokAdapter(cfg);
|
|
225
|
-
const codexAdapter = new codex.CodexAdapter(cfg);
|
|
226
|
-
|
|
227
|
-
const gatewayAdapters = cfg.gateways.map(
|
|
228
|
-
(entry) => new gateway.GatewayAdapter(entry, cfg, headless.startClaudeHeadlessRun),
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
let handle: Awaited<ReturnType<typeof daemon.startDaemon>>;
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
handle = await daemon.startDaemon({
|
|
235
|
-
socketPath: config.daemonSocketPath,
|
|
236
|
-
reporterSocketPath: config.socketPath,
|
|
237
|
-
eventsSocketPath: config.eventsSocketPath,
|
|
238
|
-
build: getBuild(),
|
|
239
|
-
adapter: claudeAdapter,
|
|
240
|
-
adapters: [claudeAdapter, grokAdapter, codexAdapter, ...gatewayAdapters],
|
|
241
|
-
dbPath: config.dbFile,
|
|
242
|
-
legacyFleetPath: config.legacyFleetFile,
|
|
243
|
-
pidPath: config.daemonPidFile,
|
|
244
|
-
hooks: cfg.hooks,
|
|
245
|
-
targets: built.targets,
|
|
246
|
-
defaultTarget: cfg.defaultTarget,
|
|
247
|
-
targetErrors,
|
|
248
|
-
principals: cfg.principals,
|
|
249
|
-
sources: sources.sources,
|
|
250
|
-
gitTransports: cfg.workspaces.gitTransports,
|
|
251
|
-
restoreBootTimeoutMs,
|
|
252
|
-
...(Number.isFinite(graceOverride) && graceOverride >= 0
|
|
253
|
-
? { tapGraceMs: graceOverride }
|
|
254
|
-
: {}),
|
|
255
|
-
...(Number.isFinite(queueBytes) && queueBytes > 0 ? { queueBytes } : {}),
|
|
256
|
-
onQuit: () => process.exit(0),
|
|
257
|
-
});
|
|
258
|
-
} catch (error) {
|
|
259
|
-
// A second daemon on the same state directory refuses to start
|
|
260
|
-
// and touches nothing; any other startup failure stays a crash.
|
|
261
|
-
if (error instanceof Error && Reflect.get(error, 'code') === 'daemon_locked') {
|
|
262
|
-
console.error(error.message);
|
|
263
|
-
process.exit(1);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
throw error;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
process.on('SIGTERM', () => {
|
|
270
|
-
void (async () => {
|
|
271
|
-
await handle.stop();
|
|
186
|
+
id: () =>
|
|
187
|
+
defineCommand({
|
|
188
|
+
meta: {
|
|
189
|
+
name: 'id',
|
|
190
|
+
description: "Print the running daemon's daemonID",
|
|
191
|
+
},
|
|
192
|
+
async run() {
|
|
193
|
+
const id = await import('./run-daemon-id');
|
|
272
194
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
195
|
+
await id.runDaemonID(getBuild());
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
276
198
|
},
|
|
277
199
|
}),
|
|
278
200
|
events: () =>
|
|
@@ -400,4 +322,151 @@ const main = defineCommand({
|
|
|
400
322
|
},
|
|
401
323
|
});
|
|
402
324
|
|
|
325
|
+
// Runs the daemon in the foreground until SIGTERM, with a TCP listener when
|
|
326
|
+
// --listen and --token-file are given.
|
|
327
|
+
async function runDaemon(listenArg: string | null, tokenFile: string | null): Promise<void> {
|
|
328
|
+
if ((listenArg === null) !== (tokenFile === null)) {
|
|
329
|
+
console.error('atc daemon: --listen and --token-file go together');
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const listen = listenArg === null ? null : parseListenAddress(listenArg);
|
|
334
|
+
|
|
335
|
+
if (listen !== null && !listen.ok) {
|
|
336
|
+
console.error(`atc daemon: ${listen.message}`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const daemon = await import('./daemon/daemon');
|
|
341
|
+
const config = await import('./shared/config');
|
|
342
|
+
const claude = await import('./agents/claude-adapter');
|
|
343
|
+
const grok = await import('./agents/grok-adapter');
|
|
344
|
+
const codex = await import('./agents/codex-adapter');
|
|
345
|
+
const gateway = await import('./agents/gateway-adapter');
|
|
346
|
+
const headless = await import('./agents/start-claude-headless-run');
|
|
347
|
+
const targets = await import('./daemon/build-execution-targets');
|
|
348
|
+
const sourceOrder = await import('./sources/build-sources');
|
|
349
|
+
const builtinSources = await import('./sources/collect-builtin-sources');
|
|
350
|
+
const zoxide = await import('./shared/collect-zoxide-dirs');
|
|
351
|
+
const home = await import('./shared/resolve-home-dir');
|
|
352
|
+
|
|
353
|
+
// Test harnesses shrink the outbound queue to force overflow
|
|
354
|
+
// deterministically; unset means the production default.
|
|
355
|
+
const queueBytes = Number(process.env['ATC_QUEUE_BYTES']);
|
|
356
|
+
const cfg = config.loadConfig();
|
|
357
|
+
const built = targets.buildExecutionTargets(cfg.targets);
|
|
358
|
+
const targetErrors = [...cfg.targetErrors, ...built.errors];
|
|
359
|
+
|
|
360
|
+
for (const error of targetErrors) {
|
|
361
|
+
const line =
|
|
362
|
+
error.scope === 'config'
|
|
363
|
+
? `${error.path} cannot be used (${error.problem}: ${error.detail}); every spawn is refused, local ones included, until it is fixed`
|
|
364
|
+
: error.problem;
|
|
365
|
+
|
|
366
|
+
console.error(`atc daemon: config: ${line}`);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const problem of [
|
|
370
|
+
...cfg.principalErrors,
|
|
371
|
+
...cfg.workspaceErrors,
|
|
372
|
+
...cfg.authProfileErrors,
|
|
373
|
+
...cfg.gatewayErrors,
|
|
374
|
+
]) {
|
|
375
|
+
console.error(`atc daemon: config: ${problem}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const sources = sourceOrder.buildSources(
|
|
379
|
+
builtinSources.collectBuiltinSources({
|
|
380
|
+
roots: cfg.dirs.roots,
|
|
381
|
+
githubOwner: cfg.workspaces.githubOwner,
|
|
382
|
+
ghBin: 'gh',
|
|
383
|
+
homeDir: home.resolveHomeDir(),
|
|
384
|
+
collectZoxideDirs: zoxide.collectZoxideDirs,
|
|
385
|
+
}),
|
|
386
|
+
cfg.workspaces.sources,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
for (const id of sources.missing) {
|
|
390
|
+
console.error(
|
|
391
|
+
`atc daemon: config: workspaces.sources holds '${id}', which this daemon cannot offer; the picker leaves it out`,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Cap on how long a fleet restore waits for one revived session to
|
|
396
|
+
// report it has booted before moving to the next. Tests pin it to
|
|
397
|
+
// keep timing deterministic; unset means the production default.
|
|
398
|
+
const capOverride = Number(process.env['ATC_RESTORE_BOOT_TIMEOUT_MS']);
|
|
399
|
+
|
|
400
|
+
const restoreBootTimeoutMs =
|
|
401
|
+
Number.isFinite(capOverride) && capOverride >= 0 ? capOverride : 15_000;
|
|
402
|
+
|
|
403
|
+
// How long a started session may go without a tap before a message
|
|
404
|
+
// to it is refused. Tests pin it to 0 to reach the refusal at once.
|
|
405
|
+
const graceOverride = Number(process.env['ATC_TAP_GRACE_MS']);
|
|
406
|
+
|
|
407
|
+
const claudeAdapter = new claude.ClaudeAdapter(cfg, headless.startClaudeHeadlessRun);
|
|
408
|
+
const grokAdapter = new grok.GrokAdapter(cfg);
|
|
409
|
+
const codexAdapter = new codex.CodexAdapter(cfg);
|
|
410
|
+
|
|
411
|
+
const gatewayAdapters = cfg.gateways.map(
|
|
412
|
+
(entry) => new gateway.GatewayAdapter(entry, cfg, headless.startClaudeHeadlessRun),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
let handle: Awaited<ReturnType<typeof daemon.startDaemon>>;
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
handle = await daemon.startDaemon({
|
|
419
|
+
socketPath: config.daemonSocketPath,
|
|
420
|
+
reporterSocketPath: config.socketPath,
|
|
421
|
+
eventsSocketPath: config.eventsSocketPath,
|
|
422
|
+
build: getBuild(),
|
|
423
|
+
adapter: claudeAdapter,
|
|
424
|
+
adapters: [claudeAdapter, grokAdapter, codexAdapter, ...gatewayAdapters],
|
|
425
|
+
dbPath: config.dbFile,
|
|
426
|
+
legacyFleetPath: config.legacyFleetFile,
|
|
427
|
+
pidPath: config.daemonPidFile,
|
|
428
|
+
hooks: cfg.hooks,
|
|
429
|
+
targets: built.targets,
|
|
430
|
+
defaultTarget: cfg.defaultTarget,
|
|
431
|
+
targetErrors,
|
|
432
|
+
principals: cfg.principals,
|
|
433
|
+
sources: sources.sources,
|
|
434
|
+
gitTransports: cfg.workspaces.gitTransports,
|
|
435
|
+
...(listen === null || tokenFile === null
|
|
436
|
+
? {}
|
|
437
|
+
: { listen: { host: listen.host, port: listen.port, tokenFile } }),
|
|
438
|
+
restoreBootTimeoutMs,
|
|
439
|
+
...(Number.isFinite(graceOverride) && graceOverride >= 0
|
|
440
|
+
? { tapGraceMs: graceOverride }
|
|
441
|
+
: {}),
|
|
442
|
+
...(Number.isFinite(queueBytes) && queueBytes > 0 ? { queueBytes } : {}),
|
|
443
|
+
onQuit: () => process.exit(0),
|
|
444
|
+
});
|
|
445
|
+
} catch (error) {
|
|
446
|
+
// A second daemon on the same state directory, or a listener whose
|
|
447
|
+
// address or token file is refused or whose bind fails, stops the start
|
|
448
|
+
// with nothing left held; any other startup failure stays a crash.
|
|
449
|
+
const code: unknown = error instanceof Error ? Reflect.get(error, 'code') : null;
|
|
450
|
+
|
|
451
|
+
if (error instanceof Error && (code === 'daemon_locked' || code === 'listen_refused')) {
|
|
452
|
+
console.error(error.message);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
process.on('SIGHUP', () => {
|
|
460
|
+
handle.refreshTokens();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
process.on('SIGTERM', () => {
|
|
464
|
+
void (async () => {
|
|
465
|
+
await handle.stop();
|
|
466
|
+
|
|
467
|
+
process.exit(0);
|
|
468
|
+
})();
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
403
472
|
await runMain(main, { rawArgs: normalizeCLIArgs(process.argv.slice(2)) });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SocketHandler } from 'bun';
|
|
1
2
|
import type { DaemonChannel } from '../protocol/daemon-channel';
|
|
2
3
|
import { DaemonError } from '../protocol/daemon-error';
|
|
3
4
|
import { LineDecoder } from '../protocol/line-decoder';
|
|
@@ -34,25 +35,30 @@ export class DaemonClient implements DaemonChannel {
|
|
|
34
35
|
// once instead of waiting on a response that can never arrive.
|
|
35
36
|
private closedReason: string | null = null;
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
// Connects to the daemon at a unix socket path, or at a TCP address.
|
|
39
|
+
static async open(
|
|
40
|
+
address: string | { readonly hostname: string; readonly port: number },
|
|
41
|
+
): Promise<DaemonClient> {
|
|
38
42
|
const client = new DaemonClient();
|
|
39
43
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
data(_s, buf) {
|
|
44
|
-
client.applyChunk(buf);
|
|
45
|
-
},
|
|
46
|
-
drain() {
|
|
47
|
-
client.queue?.drain();
|
|
48
|
-
},
|
|
49
|
-
close() {
|
|
50
|
-
client.drainPending('connection closed');
|
|
51
|
-
client.onClose();
|
|
52
|
-
},
|
|
53
|
-
error() {},
|
|
44
|
+
const handlers: SocketHandler = {
|
|
45
|
+
data(_s, buf) {
|
|
46
|
+
client.applyChunk(buf);
|
|
54
47
|
},
|
|
55
|
-
|
|
48
|
+
drain() {
|
|
49
|
+
client.queue?.drain();
|
|
50
|
+
},
|
|
51
|
+
close() {
|
|
52
|
+
client.drainPending('connection closed');
|
|
53
|
+
client.onClose();
|
|
54
|
+
},
|
|
55
|
+
error() {},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const socket =
|
|
59
|
+
typeof address === 'string'
|
|
60
|
+
? await Bun.connect({ unix: address, socket: handlers })
|
|
61
|
+
: await Bun.connect({ hostname: address.hostname, port: address.port, socket: handlers });
|
|
56
62
|
|
|
57
63
|
client.socket = socket;
|
|
58
64
|
|
|
@@ -61,8 +67,13 @@ export class DaemonClient implements DaemonChannel {
|
|
|
61
67
|
return client;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
// A handshake with a token presents it as a bearer token, which a TCP
|
|
71
|
+
// listener requires.
|
|
72
|
+
sendHello(build: string, token?: string): Promise<Readonly<Record<string, unknown>>> {
|
|
73
|
+
return this.sendRequest('daemon.hello', {
|
|
74
|
+
client: build,
|
|
75
|
+
auth: token === undefined ? { scheme: 'none' } : { scheme: 'bearer', token },
|
|
76
|
+
});
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
// A request with a principal acts as that principal, within what the
|
|
@@ -3,11 +3,12 @@ import { isRecord } from '../shared/report';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The SHA-256 of a request's params as canonical JSON: object keys sorted
|
|
6
|
-
* at every depth, with the idempotency key
|
|
7
|
-
* that differ only in key order
|
|
6
|
+
* at every depth, with the idempotency key and the replay-only flag left
|
|
7
|
+
* out. Two requests that differ only in key order, in their idempotency
|
|
8
|
+
* key, or in whether they only replay hash the same.
|
|
8
9
|
*/
|
|
9
10
|
export function buildPayloadHash(params: Readonly<Record<string, unknown>>): string {
|
|
10
|
-
const { idempotencyKey: _ignored, ...payload } = params;
|
|
11
|
+
const { idempotencyKey: _ignored, replayOnly: _replayOnly, ...payload } = params;
|
|
11
12
|
|
|
12
13
|
return createHash('sha256')
|
|
13
14
|
.update(JSON.stringify(sortKeys(payload)))
|