@zgeoff/atc 2.20.0 → 2.23.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
|
@@ -26,7 +26,7 @@ import { buildTargetForbiddenError } from './build-target-forbidden-error';
|
|
|
26
26
|
import type { DaemonContext, SpawnParams } from './daemon-context';
|
|
27
27
|
import type { TranscriptPosition } from './load-transcript-page';
|
|
28
28
|
import { parseSpawnOverrides } from './parse-spawn-overrides';
|
|
29
|
-
import
|
|
29
|
+
import { TargetAccess } from './target-access';
|
|
30
30
|
|
|
31
31
|
// The targets a request may use and the namespace of its idempotency keys.
|
|
32
32
|
interface RequestScope {
|
|
@@ -60,6 +60,19 @@ interface PeerSocket extends SocketWriter {
|
|
|
60
60
|
readonly end: () => void;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* What a connection accepted over TCP needs from its listener: the check of
|
|
65
|
+
* the bearer token its handshake presents, which answers with the matched
|
|
66
|
+
* token's fingerprint, or null for a missing or wrong token.
|
|
67
|
+
*/
|
|
68
|
+
export interface TCPPeer {
|
|
69
|
+
readonly verifyHandshake: (presented: string | null) => Promise<string | null>;
|
|
70
|
+
|
|
71
|
+
// Counts a line other than a handshake, sent before one passed, as a
|
|
72
|
+
// failed handshake from the peer's address.
|
|
73
|
+
readonly recordFailure: () => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
export class DaemonConnection {
|
|
64
77
|
private readonly peer: PeerSocket;
|
|
65
78
|
|
|
@@ -95,11 +108,37 @@ export class DaemonConnection {
|
|
|
95
108
|
|
|
96
109
|
private readonly shownRequests = new Set<string>();
|
|
97
110
|
|
|
98
|
-
|
|
111
|
+
// The listener of a connection accepted over TCP, null on the local
|
|
112
|
+
// socket.
|
|
113
|
+
private readonly tcp: TCPPeer | null;
|
|
114
|
+
|
|
115
|
+
// The fingerprint of the token a TCP handshake presented, set once the
|
|
116
|
+
// handshake passes; null before then and on the local socket.
|
|
117
|
+
private fingerprint: string | null = null;
|
|
118
|
+
|
|
119
|
+
// Whether this TCP connection has sent its one handshake.
|
|
120
|
+
private tcpHelloSent = false;
|
|
121
|
+
|
|
122
|
+
constructor(peer: PeerSocket, ctx: DaemonContext, tcp: TCPPeer | null = null) {
|
|
99
123
|
this.peer = peer;
|
|
100
124
|
this.ctx = ctx;
|
|
125
|
+
this.tcp = tcp;
|
|
101
126
|
|
|
102
127
|
this.queue = new OutboundQueue(peer, ctx.queueBytes);
|
|
128
|
+
|
|
129
|
+
// A TCP connection acts only as the principals its requests give, so
|
|
130
|
+
// the events pushed to it are limited from the first byte.
|
|
131
|
+
if (tcp !== null) {
|
|
132
|
+
this.access = new TargetAccess([]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The fingerprint of the token this connection's TCP handshake presented,
|
|
138
|
+
* or null for a connection that has not passed one.
|
|
139
|
+
*/
|
|
140
|
+
get tokenFingerprint(): string | null {
|
|
141
|
+
return this.fingerprint;
|
|
103
142
|
}
|
|
104
143
|
|
|
105
144
|
/**
|
|
@@ -110,7 +149,7 @@ export class DaemonConnection {
|
|
|
110
149
|
}
|
|
111
150
|
|
|
112
151
|
sendEvent(event: EventMsg): void {
|
|
113
|
-
if (!this.
|
|
152
|
+
if (!this.hasPassedHello()) {
|
|
114
153
|
return;
|
|
115
154
|
}
|
|
116
155
|
|
|
@@ -142,7 +181,7 @@ export class DaemonConnection {
|
|
|
142
181
|
// intermediate chunk is never dropped without that resync, because a byte
|
|
143
182
|
// stream cut mid-escape corrupts the client's terminal state.
|
|
144
183
|
sendOutput(sessionID: SessionID, event: EventMsg, byteLength: number): void {
|
|
145
|
-
if (!this.
|
|
184
|
+
if (!this.hasPassedHello() || !this.canSeeOutput(sessionID)) {
|
|
146
185
|
return;
|
|
147
186
|
}
|
|
148
187
|
|
|
@@ -159,6 +198,11 @@ export class DaemonConnection {
|
|
|
159
198
|
}
|
|
160
199
|
}
|
|
161
200
|
|
|
201
|
+
// Whether the handshake has passed: on TCP, only once its token checked out.
|
|
202
|
+
private hasPassedHello(): boolean {
|
|
203
|
+
return this.helloed && (this.tcp === null || this.fingerprint !== null);
|
|
204
|
+
}
|
|
205
|
+
|
|
162
206
|
// Decodes with state kept across reads, so a multi-byte character split
|
|
163
207
|
// between two reads decodes whole.
|
|
164
208
|
// oxlint-disable-next-line prefer-readonly-parameter-types -- a socket read buffer has no readonly form
|
|
@@ -167,7 +211,16 @@ export class DaemonConnection {
|
|
|
167
211
|
}
|
|
168
212
|
|
|
169
213
|
applyChunk(chunk: string): void {
|
|
170
|
-
|
|
214
|
+
const oversized = this.lines.pendingLength + chunk.length > MAX_LINE;
|
|
215
|
+
|
|
216
|
+
if (oversized && this.isUnauthenticatedTCP()) {
|
|
217
|
+
this.tcp?.recordFailure();
|
|
218
|
+
this.peer.end();
|
|
219
|
+
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (oversized) {
|
|
171
224
|
this.sendErr(0, 'bad_args', `line exceeds ${MAX_LINE} bytes`);
|
|
172
225
|
this.peer.end();
|
|
173
226
|
|
|
@@ -209,6 +262,14 @@ export class DaemonConnection {
|
|
|
209
262
|
|
|
210
263
|
// false: the connection is beyond recovery and gets closed.
|
|
211
264
|
private applyLine(line: string): boolean {
|
|
265
|
+
if (this.tcp !== null) {
|
|
266
|
+
const refused = this.findTCPLineRefusal(line);
|
|
267
|
+
|
|
268
|
+
if (refused === 'close') {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
212
273
|
const decoded = decodeMessage(line);
|
|
213
274
|
|
|
214
275
|
if (decoded.kind === 'malformed') {
|
|
@@ -226,7 +287,7 @@ export class DaemonConnection {
|
|
|
226
287
|
const req = decoded.msg;
|
|
227
288
|
|
|
228
289
|
if (req.m === 'daemon.hello') {
|
|
229
|
-
return this.applyHello(req);
|
|
290
|
+
return this.tcp === null ? this.applyHello(req) : this.applyTCPHello(req, this.tcp);
|
|
230
291
|
}
|
|
231
292
|
|
|
232
293
|
if (!this.helloed) {
|
|
@@ -235,6 +296,12 @@ export class DaemonConnection {
|
|
|
235
296
|
return false;
|
|
236
297
|
}
|
|
237
298
|
|
|
299
|
+
if (this.tcp !== null) {
|
|
300
|
+
this.answerAsync(req.id, () => this.applyTCPRequest(req));
|
|
301
|
+
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
238
305
|
const scope = this.findRequestScope(req);
|
|
239
306
|
|
|
240
307
|
if (scope !== null && OWNER_METHODS.has(req.m)) {
|
|
@@ -251,6 +318,82 @@ export class DaemonConnection {
|
|
|
251
318
|
return true;
|
|
252
319
|
}
|
|
253
320
|
|
|
321
|
+
// Whether a line ends this TCP connection before any answer to it. Until
|
|
322
|
+
// its token checks out, a connection gets nothing but the answer to one
|
|
323
|
+
// handshake: any other line, or a line while that handshake is still
|
|
324
|
+
// checked, counts as a failed handshake and closes it without a reply,
|
|
325
|
+
// so a peer learns nothing and floods nothing past the delay. Once in,
|
|
326
|
+
// a second handshake, which could change the principal under kept
|
|
327
|
+
// attachments, gets `unauthorized` and closes it.
|
|
328
|
+
private findTCPLineRefusal(line: string): 'close' | null {
|
|
329
|
+
if (this.isUnauthenticatedTCP()) {
|
|
330
|
+
const decoded = decodeMessage(line);
|
|
331
|
+
const isHello = decoded.kind === 'request' && decoded.msg.m === 'daemon.hello';
|
|
332
|
+
|
|
333
|
+
if (this.tcpHelloSent || !isHello) {
|
|
334
|
+
this.tcp?.recordFailure();
|
|
335
|
+
|
|
336
|
+
return 'close';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.tcpHelloSent = true;
|
|
340
|
+
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const decoded = decodeMessage(line);
|
|
345
|
+
|
|
346
|
+
if (decoded.kind === 'request' && decoded.msg.m === 'daemon.hello') {
|
|
347
|
+
this.sendErr(decoded.msg.id, 'unauthorized', 'a TCP connection takes one handshake');
|
|
348
|
+
|
|
349
|
+
return 'close';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Whether this is a TCP connection whose handshake has not passed.
|
|
356
|
+
private isUnauthenticatedTCP(): boolean {
|
|
357
|
+
return this.tcp !== null && this.fingerprint === null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// A request over TCP runs only once the handshake has passed, only as a
|
|
361
|
+
// principal the config lists, and never as the daemon's owner, so it
|
|
362
|
+
// always runs on the context scoped to that principal.
|
|
363
|
+
private async applyTCPRequest(req: RequestMsg): Promise<void> {
|
|
364
|
+
await this.helloAnswered;
|
|
365
|
+
|
|
366
|
+
if (this.fingerprint === null) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (OWNER_METHODS.has(req.m)) {
|
|
371
|
+
this.sendErr(req.id, 'unauthorized', `${req.m} is open to the daemon's owner only`);
|
|
372
|
+
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (req.as === undefined) {
|
|
377
|
+
this.sendErr(req.id, 'unauthorized', 'a request over TCP must act as a principal with as');
|
|
378
|
+
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!this.ctx.hasListedPrincipal(req.as)) {
|
|
383
|
+
this.sendErr(req.id, 'unauthorized', `principal '${req.as}' is not listed in principals`);
|
|
384
|
+
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const scope = this.findRequestScope(req);
|
|
389
|
+
|
|
390
|
+
if (scope === null) {
|
|
391
|
+
throw new DaemonError('internal', 'a request over TCP resolved to the owner scope');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
await this.applyRequest(req, buildScopedContext(this.ctx, scope.access, scope.keyNamespace));
|
|
395
|
+
}
|
|
396
|
+
|
|
254
397
|
// The targets a request may use and the namespace its idempotency keys
|
|
255
398
|
// live in: the request's own principal, limited to what the connection's
|
|
256
399
|
// may use, else the connection's. Null is the daemon's owner. The owner
|
|
@@ -804,10 +947,20 @@ export class DaemonConnection {
|
|
|
804
947
|
};
|
|
805
948
|
};
|
|
806
949
|
|
|
950
|
+
if (data.replayOnly === true && data.idempotencyKey === undefined) {
|
|
951
|
+
this.sendErr(req.id, 'bad_args', 'replayOnly needs an idempotencyKey');
|
|
952
|
+
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
807
956
|
const keyed =
|
|
808
957
|
data.idempotencyKey === undefined
|
|
809
958
|
? null
|
|
810
|
-
: {
|
|
959
|
+
: {
|
|
960
|
+
key: data.idempotencyKey,
|
|
961
|
+
payloadHash: buildPayloadHash(data),
|
|
962
|
+
replayOnly: data.replayOnly === true,
|
|
963
|
+
};
|
|
811
964
|
|
|
812
965
|
const spawned = await ctx.spawnSession(plan, keyed, null);
|
|
813
966
|
|
|
@@ -1224,10 +1377,20 @@ export class DaemonConnection {
|
|
|
1224
1377
|
|
|
1225
1378
|
const sessionID = parsed.data.session;
|
|
1226
1379
|
|
|
1380
|
+
if (parsed.data.replayOnly === true && parsed.data.idempotencyKey === undefined) {
|
|
1381
|
+
this.sendErr(req.id, 'bad_args', 'replayOnly needs an idempotencyKey');
|
|
1382
|
+
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1227
1386
|
const keyed =
|
|
1228
1387
|
parsed.data.idempotencyKey === undefined
|
|
1229
1388
|
? null
|
|
1230
|
-
: {
|
|
1389
|
+
: {
|
|
1390
|
+
key: parsed.data.idempotencyKey,
|
|
1391
|
+
payloadHash: buildPayloadHash(parsed.data),
|
|
1392
|
+
replayOnly: parsed.data.replayOnly === true,
|
|
1393
|
+
};
|
|
1231
1394
|
|
|
1232
1395
|
const result = await ctx.writeSessionMessage(
|
|
1233
1396
|
sessionID,
|
|
@@ -1405,18 +1568,7 @@ export class DaemonConnection {
|
|
|
1405
1568
|
const principal = parsedHello.ok ? (parsedHello.data.principal ?? null) : null;
|
|
1406
1569
|
|
|
1407
1570
|
if (principal !== null) {
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
this.principal = principal;
|
|
1411
|
-
this.access = access;
|
|
1412
|
-
|
|
1413
|
-
for (const session of this.ctx.collectSessions()) {
|
|
1414
|
-
const view = this.ctx.canSeeSession(session.id, access)
|
|
1415
|
-
? this.shownSessions
|
|
1416
|
-
: this.withheldSessions;
|
|
1417
|
-
|
|
1418
|
-
view.add(session.id);
|
|
1419
|
-
}
|
|
1571
|
+
this.setPrincipal(principal);
|
|
1420
1572
|
}
|
|
1421
1573
|
|
|
1422
1574
|
if (req.v !== PROTOCOL_V) {
|
|
@@ -1437,12 +1589,98 @@ export class DaemonConnection {
|
|
|
1437
1589
|
return true;
|
|
1438
1590
|
}
|
|
1439
1591
|
|
|
1592
|
+
// A TCP handshake must carry a bearer token the listener holds, checked
|
|
1593
|
+
// before anything else in it, so a peer without one learns nothing about
|
|
1594
|
+
// the daemon. A wrong or missing token, or a principal the config does
|
|
1595
|
+
// not list, gets `unauthorized` and the connection closes. Requests that
|
|
1596
|
+
// arrive while the check runs wait behind it.
|
|
1597
|
+
private applyTCPHello(req: RequestMsg, tcp: TCPPeer): boolean {
|
|
1598
|
+
this.helloed = true;
|
|
1599
|
+
|
|
1600
|
+
this.helloAnswered = (async () => {
|
|
1601
|
+
const fingerprint = await tcp.verifyHandshake(findBearerToken(req.p));
|
|
1602
|
+
|
|
1603
|
+
if (fingerprint === null) {
|
|
1604
|
+
this.sendErr(req.id, 'unauthorized', 'daemon.hello over TCP needs a valid bearer token');
|
|
1605
|
+
this.peer.end();
|
|
1606
|
+
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
const parsedHello = parseRequestParams('daemon.hello', req.p);
|
|
1611
|
+
|
|
1612
|
+
if (!parsedHello.ok) {
|
|
1613
|
+
this.sendErr(req.id, 'bad_args', parsedHello.message);
|
|
1614
|
+
this.peer.end();
|
|
1615
|
+
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const principal = parsedHello.data.principal ?? null;
|
|
1620
|
+
|
|
1621
|
+
if (principal !== null && !this.ctx.hasListedPrincipal(principal)) {
|
|
1622
|
+
this.sendErr(
|
|
1623
|
+
req.id,
|
|
1624
|
+
'unauthorized',
|
|
1625
|
+
`principal '${principal}' is not listed in principals`,
|
|
1626
|
+
);
|
|
1627
|
+
|
|
1628
|
+
this.peer.end();
|
|
1629
|
+
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (req.v !== PROTOCOL_V) {
|
|
1634
|
+
this.sendErr(
|
|
1635
|
+
req.id,
|
|
1636
|
+
'protocol_mismatch',
|
|
1637
|
+
`${parsedHello.data.client} speaks protocol v${req.v}, daemon ${this.ctx.build} speaks v${PROTOCOL_V}; restart the daemon so both run the same build`,
|
|
1638
|
+
);
|
|
1639
|
+
|
|
1640
|
+
this.peer.end();
|
|
1641
|
+
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
if (principal !== null) {
|
|
1646
|
+
this.setPrincipal(principal);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
this.fingerprint = fingerprint;
|
|
1650
|
+
|
|
1651
|
+
await this.sendHelloOk(req.id);
|
|
1652
|
+
})();
|
|
1653
|
+
|
|
1654
|
+
this.answerAsync(req.id, () => this.helloAnswered);
|
|
1655
|
+
|
|
1656
|
+
return true;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// Makes the connection act as the principal, its view starting as the
|
|
1660
|
+
// sessions the principal may see now, so one that leaves its reach later
|
|
1661
|
+
// is pushed as removed.
|
|
1662
|
+
private setPrincipal(principal: string): void {
|
|
1663
|
+
const access = this.ctx.buildTargetAccess(principal);
|
|
1664
|
+
|
|
1665
|
+
this.principal = principal;
|
|
1666
|
+
this.access = access;
|
|
1667
|
+
|
|
1668
|
+
for (const session of this.ctx.collectSessions()) {
|
|
1669
|
+
const view = this.ctx.canSeeSession(session.id, access)
|
|
1670
|
+
? this.shownSessions
|
|
1671
|
+
: this.withheldSessions;
|
|
1672
|
+
|
|
1673
|
+
view.add(session.id);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1440
1677
|
private async sendHelloOk(id: number): Promise<void> {
|
|
1441
1678
|
this.sendOk(id, {
|
|
1442
1679
|
daemon: this.ctx.build,
|
|
1443
1680
|
daemonID: this.ctx.daemonID,
|
|
1444
1681
|
limits: { maxLine: MAX_LINE, maxChunk: MAX_CHUNK },
|
|
1445
1682
|
features: DAEMON_FEATURES,
|
|
1683
|
+
idempotency: { completedRetentionMs: this.ctx.idempotencyRetentionMs },
|
|
1446
1684
|
lastUsedAgent: await this.ctx.loadLastUsedAgent(),
|
|
1447
1685
|
});
|
|
1448
1686
|
}
|
|
@@ -1467,6 +1705,18 @@ export class DaemonConnection {
|
|
|
1467
1705
|
}
|
|
1468
1706
|
}
|
|
1469
1707
|
|
|
1708
|
+
// The token of a handshake's `auth: { scheme: "bearer", token }`, or null
|
|
1709
|
+
// when its params hold none.
|
|
1710
|
+
function findBearerToken(p: Readonly<Record<string, unknown>> | undefined): string | null {
|
|
1711
|
+
const auth = p?.['auth'];
|
|
1712
|
+
|
|
1713
|
+
if (!isRecord(auth) || auth['scheme'] !== 'bearer' || typeof auth['token'] !== 'string') {
|
|
1714
|
+
return null;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
return auth['token'];
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1470
1720
|
// The session an event belongs to, from its session id or the session it
|
|
1471
1721
|
// adds, or null for an event of no session.
|
|
1472
1722
|
function findEventSession(event: EventMsg): SessionID | null {
|
|
@@ -122,6 +122,10 @@ export interface TapClient {
|
|
|
122
122
|
export interface DaemonContext {
|
|
123
123
|
readonly build: string;
|
|
124
124
|
readonly daemonID: DaemonID;
|
|
125
|
+
|
|
126
|
+
// How long the daemon keeps a completed idempotency key, which the
|
|
127
|
+
// handshake announces so a caller knows how long a retry stays deduplicated.
|
|
128
|
+
readonly idempotencyRetentionMs: number;
|
|
125
129
|
readonly collectSessions: () => SessionDescriptor[];
|
|
126
130
|
|
|
127
131
|
// The directories spawns ran in, most recent first, leaving out each one
|
|
@@ -135,6 +139,10 @@ export interface DaemonContext {
|
|
|
135
139
|
// The targets a principal may use.
|
|
136
140
|
readonly buildTargetAccess: (principal: string) => TargetAccess;
|
|
137
141
|
|
|
142
|
+
// Whether the config's principals key lists the principal, which a
|
|
143
|
+
// request over TCP must act as.
|
|
144
|
+
readonly hasListedPrincipal: (principal: string) => boolean;
|
|
145
|
+
|
|
138
146
|
// The target and identity a session is bound to, or null when no
|
|
139
147
|
// session holds the id.
|
|
140
148
|
readonly findSessionGrant: (id: SessionID) => TargetGrant | null;
|