@zgeoff/atc 2.10.4 → 2.12.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 +3 -3
- package/package.json +1 -1
- package/src/agents/agent-adapter.ts +72 -0
- package/src/agents/atc-bridge-files.ts +1 -1
- package/src/agents/build-args-without-flags.ts +25 -0
- package/src/agents/build-claude-override-args.ts +26 -0
- package/src/agents/claude-adapter.ts +52 -1
- package/src/agents/claude-effort-levels.ts +5 -0
- package/src/agents/codex-adapter.ts +46 -1
- package/src/agents/find-flag-value.ts +27 -0
- package/src/agents/gateway-adapter.ts +75 -1
- package/src/agents/grok-adapter.ts +22 -0
- package/src/cli.ts +22 -0
- package/src/client/boot-daemon.ts +6 -0
- package/src/client/daemon-client.ts +14 -2
- package/src/client/spawn-picker.ts +27 -4
- package/src/daemon/build-agent-list.ts +94 -0
- package/src/daemon/build-config-revision.ts +27 -0
- package/src/daemon/build-execution-targets.ts +34 -0
- package/src/daemon/build-fleet-events.ts +2 -1
- package/src/daemon/build-payload-hash.ts +31 -0
- package/src/daemon/build-scoped-context.ts +232 -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 +371 -99
- package/src/daemon/daemon.ts +537 -90
- package/src/daemon/effect-remains-error.ts +13 -0
- package/src/daemon/execution-provider.ts +103 -0
- package/src/daemon/find-execution-refusal.ts +104 -0
- package/src/daemon/idempotency-ledger.ts +164 -0
- package/src/daemon/local-pty-provider.ts +83 -0
- package/src/daemon/mint-session-id.ts +5 -5
- package/src/daemon/parse-report.ts +29 -3
- package/src/daemon/parse-spawn-overrides.ts +94 -0
- package/src/daemon/permission-registry.ts +14 -4
- package/src/daemon/restore-fleet.ts +66 -19
- package/src/daemon/session-runtime.ts +10 -0
- package/src/daemon/sessions.ts +287 -65
- package/src/daemon/start-headless-run.ts +11 -0
- package/src/daemon/start-headless-turn.ts +11 -2
- package/src/daemon/target-access.ts +36 -0
- package/src/mcp/answer-mcp-request.ts +12 -4
- package/src/mcp/answer-rpc-request.ts +40 -6
- 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 +88 -7
- package/src/mcp/mcp-tools.ts +245 -24
- package/src/mcp/parse-idempotency-key.ts +29 -0
- package/src/mcp/reconnecting-caller.ts +77 -15
- package/src/mcp/require-daemon-features.ts +40 -0
- package/src/mcp/run-tool.ts +139 -35
- package/src/mcp/start-mcp-http-server.ts +130 -24
- package/src/mcp/types.ts +9 -1
- package/src/mcp-http-server.ts +6 -1
- package/src/mcp-server.ts +13 -1
- package/src/protocol/daemon-error.ts +5 -1
- package/src/protocol/daemon-features.ts +47 -0
- package/src/protocol/parse-daemon-features.ts +19 -0
- package/src/protocol/protocol.ts +44 -11
- package/src/protocol/request-param-schemas.ts +78 -12
- package/src/report.ts +20 -3
- package/src/shared/agent-session-id.ts +1 -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/sort-json-keys.ts +19 -0
- package/src/shared/to-daemon-id.ts +11 -0
- package/src/store/fleet-entry.ts +42 -9
- package/src/store/idempotency-record.ts +48 -0
- package/src/store/message-owner.ts +1 -1
- package/src/store/message-record.ts +3 -0
- package/src/store/run-migrations.ts +272 -7
- package/src/store/state-store.ts +503 -34
- package/src/workspace/check-workspace-completeness.ts +90 -0
- package/src/workspace/create-workspace-clone.ts +239 -0
- package/src/workspace/normalize-git-url.ts +59 -0
- package/src/workspace/read-workspace-tar.ts +38 -0
- package/src/workspace/resolve-path-source.ts +170 -0
- package/src/workspace/run-git.ts +103 -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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
|
-
import type { AgentAdapter, AgentID, SpawnOptions } from '../agents/agent-adapter';
|
|
2
|
+
import type { AgentAdapter, AgentID, SpawnOptions, SpawnOverrides } from '../agents/agent-adapter';
|
|
3
|
+
import { DaemonError } from '../protocol/daemon-error';
|
|
4
|
+
import { DAEMON_FEATURES } from '../protocol/daemon-features';
|
|
3
5
|
import { decodeCursor } from '../protocol/decode-cursor';
|
|
4
6
|
import { encodeCursor } from '../protocol/encode-cursor';
|
|
5
7
|
import { OutboundQueue } from '../protocol/outbound-queue';
|
|
@@ -13,18 +15,29 @@ import {
|
|
|
13
15
|
encodeMessage,
|
|
14
16
|
} from '../protocol/protocol';
|
|
15
17
|
import type { ErrorCode, EventMsg, RequestMsg } from '../protocol/protocol';
|
|
18
|
+
import type { TargetConfigError } from '../shared/collect-targets';
|
|
19
|
+
import type { DaemonID } from '../shared/daemon-id';
|
|
16
20
|
import type { MessageID } from '../shared/message-id';
|
|
21
|
+
import { isRecord } from '../shared/report';
|
|
17
22
|
import type { SessionID } from '../shared/session-id';
|
|
23
|
+
import { toSessionID } from '../shared/to-session-id';
|
|
18
24
|
import type { FleetEntry } from '../store/fleet-entry';
|
|
19
25
|
import type { MessageRecord } from '../store/message-record';
|
|
20
26
|
import type { Dims } from './attach-registry';
|
|
27
|
+
import type { AgentEntry } from './build-agent-list';
|
|
21
28
|
import type { FleetEvent } from './build-fleet-events';
|
|
29
|
+
import { buildPayloadHash } from './build-payload-hash';
|
|
30
|
+
import { buildScopedContext } from './build-scoped-context';
|
|
31
|
+
import type { TargetEntry } from './build-target-list';
|
|
32
|
+
import type { KeyedRequest } from './idempotency-ledger';
|
|
22
33
|
import type { TranscriptPage, TranscriptPosition } from './load-transcript-page';
|
|
34
|
+
import { parseSpawnOverrides } from './parse-spawn-overrides';
|
|
23
35
|
import type { AnswerResult } from './permission-registry';
|
|
24
36
|
import type { ScreenText } from './screen-model';
|
|
25
37
|
import type { SessionDescriptor } from './sessions';
|
|
38
|
+
import type { TargetAccess, TargetGrant } from './target-access';
|
|
26
39
|
|
|
27
|
-
interface SpawnParams {
|
|
40
|
+
export interface SpawnParams {
|
|
28
41
|
readonly cwd: string;
|
|
29
42
|
readonly name: string;
|
|
30
43
|
readonly prompt: string;
|
|
@@ -34,6 +47,8 @@ interface SpawnParams {
|
|
|
34
47
|
readonly namedBy: 'user' | 'auto';
|
|
35
48
|
readonly agent: AgentID;
|
|
36
49
|
readonly parent: SessionID | null;
|
|
50
|
+
readonly overrides: SpawnOverrides;
|
|
51
|
+
readonly target: string;
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
interface SessionRecord {
|
|
@@ -53,12 +68,46 @@ interface SessionTranscriptRead {
|
|
|
53
68
|
|
|
54
69
|
export interface DaemonContext {
|
|
55
70
|
readonly build: string;
|
|
71
|
+
readonly daemonID: DaemonID;
|
|
56
72
|
readonly collectSessions: () => SessionDescriptor[];
|
|
57
73
|
readonly collectSpawnDirs: () => Promise<string[]>;
|
|
74
|
+
readonly collectAgents: () => AgentList;
|
|
58
75
|
readonly collectFleet: () => Promise<FleetEntry[]>;
|
|
59
76
|
readonly loadLastUsedAgent: () => Promise<AgentID>;
|
|
60
77
|
readonly findAdapter: (id: AgentID) => AgentAdapter | null;
|
|
61
|
-
|
|
78
|
+
|
|
79
|
+
// The targets a principal may use.
|
|
80
|
+
readonly buildTargetAccess: (principal: string) => TargetAccess;
|
|
81
|
+
|
|
82
|
+
// The target and identity a session is bound to, or null when no
|
|
83
|
+
// session holds the id.
|
|
84
|
+
readonly findSessionGrant: (id: SessionID) => TargetGrant | null;
|
|
85
|
+
|
|
86
|
+
// The identity a target holds now, or null when the config holds no such
|
|
87
|
+
// target.
|
|
88
|
+
readonly findTargetIdentity: (target: string) => string | null;
|
|
89
|
+
|
|
90
|
+
// The ids of a session's sub-sessions.
|
|
91
|
+
readonly collectChildIDs: (id: SessionID) => SessionID[];
|
|
92
|
+
|
|
93
|
+
// The session a permission request belongs to, answered or not, or null
|
|
94
|
+
// for an unknown request.
|
|
95
|
+
readonly findPermissionSession: (request: string) => SessionID | null;
|
|
96
|
+
|
|
97
|
+
// The target a spawn runs on: the one it names, else the default. Throws
|
|
98
|
+
// the refusal for a target the spawn cannot run on, and for a spawn
|
|
99
|
+
// without a target when the config gives no default.
|
|
100
|
+
readonly resolveSpawnTarget: (requested: string | undefined) => string;
|
|
101
|
+
|
|
102
|
+
// Runs the plan, which throws the refusal for a spawn it refuses, then
|
|
103
|
+
// spawns. Answers with the `session.spawn` ok payload, which a keyed
|
|
104
|
+
// retry replays as the first spawn answered it while the access, when
|
|
105
|
+
// there is one, still reaches the target the spawn's session was bound to.
|
|
106
|
+
readonly spawnSession: (
|
|
107
|
+
plan: () => SpawnParams,
|
|
108
|
+
keyed: KeyedRequest | null,
|
|
109
|
+
access: TargetAccess | null,
|
|
110
|
+
) => Promise<Readonly<Record<string, unknown>>>;
|
|
62
111
|
readonly killSession: (id: SessionID) => Promise<boolean>;
|
|
63
112
|
readonly updateSession: (id: SessionID, name?: string, pinned?: boolean) => boolean | 'child_pin';
|
|
64
113
|
readonly quitDaemon: () => void;
|
|
@@ -97,17 +146,26 @@ export interface DaemonContext {
|
|
|
97
146
|
from: TranscriptPosition | null,
|
|
98
147
|
limit: number,
|
|
99
148
|
) => Promise<SessionTranscriptRead | 'missing' | 'unsupported'>;
|
|
149
|
+
|
|
150
|
+
// Reads the trail, leaving out each event of a session outside the access
|
|
151
|
+
// when there is one.
|
|
100
152
|
readonly readEvents: (
|
|
101
153
|
afterID: number | null,
|
|
102
154
|
limit: number,
|
|
103
155
|
waitMs: number,
|
|
104
|
-
|
|
156
|
+
sessionID: SessionID | null,
|
|
157
|
+
access: TargetAccess | null,
|
|
158
|
+
) => Promise<EventsPage>;
|
|
159
|
+
|
|
160
|
+
// Answers with the `session.message` ok payload, which a keyed retry
|
|
161
|
+
// replays with the message's current status, or with the refusal.
|
|
105
162
|
readonly writeSessionMessage: (
|
|
106
163
|
sessionID: SessionID,
|
|
107
164
|
from: string,
|
|
108
165
|
text: string,
|
|
109
|
-
|
|
110
|
-
|
|
166
|
+
keyed: KeyedRequest | null,
|
|
167
|
+
) => Promise<Readonly<Record<string, unknown>> | MessageRefusal>;
|
|
168
|
+
readonly readMessage: (messageID: MessageID, waitMs: number) => Promise<MessageView | null>;
|
|
111
169
|
readonly attachTap: (client: TapClient, sessionID: SessionID) => 'ok' | 'missing' | 'unsupported';
|
|
112
170
|
readonly ackMessage: (
|
|
113
171
|
client: TapClient,
|
|
@@ -122,18 +180,55 @@ export interface OutputClient {
|
|
|
122
180
|
readonly sendOutput: (sessionID: SessionID, event: EventMsg, byteLength: number) => void;
|
|
123
181
|
}
|
|
124
182
|
|
|
125
|
-
//
|
|
126
|
-
//
|
|
183
|
+
// The `agents.list` answer: the host the daemon runs on, each agent, each
|
|
184
|
+
// execution target, what a spawn without either runs with, a digest of
|
|
185
|
+
// the target config, and the target config problems the daemon started
|
|
186
|
+
// with.
|
|
187
|
+
interface AgentList {
|
|
188
|
+
readonly daemon: {
|
|
189
|
+
readonly hostname: string;
|
|
190
|
+
readonly platform: string;
|
|
191
|
+
readonly arch: string;
|
|
192
|
+
readonly build: string;
|
|
193
|
+
};
|
|
194
|
+
readonly agents: readonly AgentEntry[];
|
|
195
|
+
readonly targets: readonly TargetEntry[];
|
|
196
|
+
readonly spawnDefaults: { readonly agent: AgentID; readonly target: string | null };
|
|
197
|
+
readonly configRevision: string;
|
|
198
|
+
readonly targetErrors: readonly TargetConfigError[];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// One `events.read` answer: the events, and whether more follow them.
|
|
202
|
+
interface EventsPage {
|
|
203
|
+
readonly events: readonly FleetEvent[];
|
|
204
|
+
readonly more: boolean;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// One message as `message.get` reports it: the session it belongs to now,
|
|
208
|
+
// every field of the record, and the other messages its turn answered.
|
|
127
209
|
interface MessageView {
|
|
128
210
|
readonly session: SessionID;
|
|
129
211
|
readonly record: MessageRecord;
|
|
212
|
+
readonly answeredWith: readonly MessageID[];
|
|
130
213
|
}
|
|
131
214
|
|
|
215
|
+
// Why a session refuses a message before the daemon accepts it.
|
|
216
|
+
export type MessageRefusal = 'missing' | 'dead' | 'unsupported' | 'no_tap';
|
|
217
|
+
|
|
132
218
|
// The slice of a connection a tap subscription needs.
|
|
133
219
|
export interface TapClient {
|
|
134
220
|
readonly sendEvent: (event: EventMsg) => void;
|
|
135
221
|
}
|
|
136
222
|
|
|
223
|
+
// The targets a request may use and the namespace of its idempotency keys.
|
|
224
|
+
interface RequestScope {
|
|
225
|
+
readonly access: TargetAccess;
|
|
226
|
+
readonly keyNamespace: string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// The requests that act on the whole daemon, which only its owner may make.
|
|
230
|
+
const OWNER_METHODS: ReadonlySet<string> = new Set(['daemon.quit', 'fleet.restore']);
|
|
231
|
+
|
|
137
232
|
interface PeerSocket extends SocketWriter {
|
|
138
233
|
readonly end: () => void;
|
|
139
234
|
}
|
|
@@ -158,6 +253,17 @@ export class DaemonConnection {
|
|
|
158
253
|
|
|
159
254
|
private readonly desynced = new Map<SessionID, number>();
|
|
160
255
|
|
|
256
|
+
// The principal the whole connection acts as, from its handshake, and the
|
|
257
|
+
// targets that principal may use; null for the daemon's owner, whose
|
|
258
|
+
// reach has no limit.
|
|
259
|
+
private principal: string | null = null;
|
|
260
|
+
|
|
261
|
+
private access: TargetAccess | null = null;
|
|
262
|
+
|
|
263
|
+
// The sessions and permission requests this limited connection was shown,
|
|
264
|
+
// so the events that end them reach it after the session is gone.
|
|
265
|
+
private readonly shown = new Set<string>();
|
|
266
|
+
|
|
161
267
|
constructor(peer: PeerSocket, ctx: DaemonContext) {
|
|
162
268
|
this.peer = peer;
|
|
163
269
|
this.ctx = ctx;
|
|
@@ -173,7 +279,11 @@ export class DaemonConnection {
|
|
|
173
279
|
}
|
|
174
280
|
|
|
175
281
|
sendEvent(event: EventMsg): void {
|
|
176
|
-
if (this.helloed
|
|
282
|
+
if (!this.helloed || !this.canSeeEvent(event)) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!this.queue.send(encodeMessage(event))) {
|
|
177
287
|
this.peer.end();
|
|
178
288
|
}
|
|
179
289
|
}
|
|
@@ -276,18 +386,88 @@ export class DaemonConnection {
|
|
|
276
386
|
return false;
|
|
277
387
|
}
|
|
278
388
|
|
|
279
|
-
|
|
389
|
+
const scope = this.findRequestScope(req);
|
|
390
|
+
|
|
391
|
+
if (scope !== null && OWNER_METHODS.has(req.m)) {
|
|
392
|
+
this.sendErr(req.id, 'unauthorized', `${req.m} is open to the daemon's owner only`);
|
|
393
|
+
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const ctx =
|
|
398
|
+
scope === null ? this.ctx : buildScopedContext(this.ctx, scope.access, scope.keyNamespace);
|
|
399
|
+
|
|
400
|
+
this.answerAsync(req.id, () => this.applyRequest(req, ctx));
|
|
401
|
+
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// The targets a request may use and the namespace its idempotency keys
|
|
406
|
+
// live in: the request's own principal, limited to what the connection's
|
|
407
|
+
// may use, else the connection's. Null is the daemon's owner. The owner
|
|
408
|
+
// may act as any principal, that principal's keys included; a connection
|
|
409
|
+
// that acts as a principal always keeps its own keys, whatever principal
|
|
410
|
+
// a request acts as.
|
|
411
|
+
private findRequestScope(req: RequestMsg): RequestScope | null {
|
|
412
|
+
if (req.as === undefined) {
|
|
413
|
+
return this.principal === null || this.access === null
|
|
414
|
+
? null
|
|
415
|
+
: { access: this.access, keyNamespace: `client:${this.principal}` };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const access = this.ctx.buildTargetAccess(req.as);
|
|
419
|
+
|
|
420
|
+
if (this.principal === null || this.access === null) {
|
|
421
|
+
return { access, keyNamespace: `client:${req.as}` };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { access: this.access.merge(access), keyNamespace: `client:${this.principal}` };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Whether this connection may see an event: always for the daemon's
|
|
428
|
+
// owner, and otherwise only for an event of a session the connection may
|
|
429
|
+
// see or was already shown.
|
|
430
|
+
private canSeeEvent(event: EventMsg): boolean {
|
|
431
|
+
if (this.access === null) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const sessionID = findEventSession(event);
|
|
436
|
+
const request = typeof event['request'] === 'string' ? event['request'] : null;
|
|
437
|
+
|
|
438
|
+
if (sessionID === null) {
|
|
439
|
+
return request !== null && this.shown.has(`request:${request}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const grant = this.ctx.findSessionGrant(sessionID);
|
|
443
|
+
|
|
444
|
+
if (grant === null || !this.access.canUse(grant)) {
|
|
445
|
+
return this.shown.has(`session:${sessionID}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.shown.add(`session:${sessionID}`);
|
|
449
|
+
|
|
450
|
+
if (request !== null) {
|
|
451
|
+
this.shown.add(`request:${request}`);
|
|
452
|
+
}
|
|
280
453
|
|
|
281
454
|
return true;
|
|
282
455
|
}
|
|
283
456
|
|
|
284
457
|
// A store query that rejects must end one request, never the daemon: a
|
|
285
|
-
// floating promise here would take the whole process down with it.
|
|
458
|
+
// floating promise here would take the whole process down with it. A
|
|
459
|
+
// rejection that carries a protocol error code answers with that code.
|
|
286
460
|
private answerAsync(id: number, answered: () => Promise<void>): void {
|
|
287
461
|
void (async () => {
|
|
288
462
|
try {
|
|
289
463
|
await answered();
|
|
290
464
|
} catch (error) {
|
|
465
|
+
if (error instanceof DaemonError) {
|
|
466
|
+
this.sendErr(id, error.code, error.message, error.data);
|
|
467
|
+
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
291
471
|
const reason = error instanceof Error ? error.message : String(error);
|
|
292
472
|
|
|
293
473
|
this.sendErr(id, 'internal', reason);
|
|
@@ -295,7 +475,7 @@ export class DaemonConnection {
|
|
|
295
475
|
})();
|
|
296
476
|
}
|
|
297
477
|
|
|
298
|
-
private async applyRequest(req: RequestMsg): Promise<void> {
|
|
478
|
+
private async applyRequest(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
299
479
|
await this.helloAnswered;
|
|
300
480
|
|
|
301
481
|
switch (req.m) {
|
|
@@ -305,28 +485,33 @@ export class DaemonConnection {
|
|
|
305
485
|
return;
|
|
306
486
|
}
|
|
307
487
|
case 'session.list': {
|
|
308
|
-
this.sendOk(req.id, { sessions:
|
|
488
|
+
this.sendOk(req.id, { sessions: ctx.collectSessions() });
|
|
489
|
+
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
case 'agents.list': {
|
|
493
|
+
this.sendOk(req.id, { ...ctx.collectAgents() });
|
|
309
494
|
|
|
310
495
|
return;
|
|
311
496
|
}
|
|
312
497
|
case 'dirs.list': {
|
|
313
|
-
this.sendOk(req.id, { dirs: await
|
|
498
|
+
this.sendOk(req.id, { dirs: await ctx.collectSpawnDirs() });
|
|
314
499
|
|
|
315
500
|
return;
|
|
316
501
|
}
|
|
317
502
|
case 'fleet.list': {
|
|
318
|
-
this.sendOk(req.id, { fleet: await
|
|
503
|
+
this.sendOk(req.id, { fleet: await ctx.collectFleet() });
|
|
319
504
|
|
|
320
505
|
return;
|
|
321
506
|
}
|
|
322
507
|
case 'session.spawn': {
|
|
323
|
-
this.applySpawn(req);
|
|
508
|
+
await this.applySpawn(req, ctx);
|
|
324
509
|
|
|
325
510
|
return;
|
|
326
511
|
}
|
|
327
512
|
case 'daemon.quit': {
|
|
328
513
|
this.sendOk(req.id, {});
|
|
329
|
-
|
|
514
|
+
ctx.quitDaemon();
|
|
330
515
|
|
|
331
516
|
return;
|
|
332
517
|
}
|
|
@@ -340,7 +525,7 @@ export class DaemonConnection {
|
|
|
340
525
|
}
|
|
341
526
|
|
|
342
527
|
const sessionID = parsed.data.session;
|
|
343
|
-
const updated =
|
|
528
|
+
const updated = ctx.updateSession(sessionID, parsed.data.name, parsed.data.pinned);
|
|
344
529
|
|
|
345
530
|
if (updated === 'child_pin') {
|
|
346
531
|
this.sendErr(
|
|
@@ -357,12 +542,12 @@ export class DaemonConnection {
|
|
|
357
542
|
return;
|
|
358
543
|
}
|
|
359
544
|
case 'session.kill': {
|
|
360
|
-
await this.applySessionVerb(req, 'session.kill',
|
|
545
|
+
await this.applySessionVerb(req, 'session.kill', ctx.killSession);
|
|
361
546
|
|
|
362
547
|
return;
|
|
363
548
|
}
|
|
364
549
|
case 'session.ack': {
|
|
365
|
-
await this.applySessionVerb(req, 'session.ack',
|
|
550
|
+
await this.applySessionVerb(req, 'session.ack', ctx.ackSession);
|
|
366
551
|
|
|
367
552
|
return;
|
|
368
553
|
}
|
|
@@ -376,7 +561,7 @@ export class DaemonConnection {
|
|
|
376
561
|
}
|
|
377
562
|
|
|
378
563
|
const id = parsed.data.session;
|
|
379
|
-
const command =
|
|
564
|
+
const command = ctx.buildResumeCommand(id);
|
|
380
565
|
|
|
381
566
|
if (command === null) {
|
|
382
567
|
this.sendErr(req.id, 'no_such_session', `no session '${id}'`);
|
|
@@ -397,7 +582,7 @@ export class DaemonConnection {
|
|
|
397
582
|
|
|
398
583
|
const id = parsed.data.session;
|
|
399
584
|
|
|
400
|
-
const screen = await
|
|
585
|
+
const screen = await ctx.readSessionScreen(id);
|
|
401
586
|
|
|
402
587
|
if (screen === 'missing') {
|
|
403
588
|
this.sendErr(req.id, 'no_such_session', `no session '${id}'`);
|
|
@@ -419,7 +604,7 @@ export class DaemonConnection {
|
|
|
419
604
|
}
|
|
420
605
|
|
|
421
606
|
const sessionID = parsed.data.session;
|
|
422
|
-
const result =
|
|
607
|
+
const result = ctx.ejectSession(sessionID, parsed.data.prompt);
|
|
423
608
|
|
|
424
609
|
if (result === 'ok') {
|
|
425
610
|
this.sendOk(req.id, {});
|
|
@@ -451,7 +636,7 @@ export class DaemonConnection {
|
|
|
451
636
|
}
|
|
452
637
|
|
|
453
638
|
const sessionID = parsed.data.session;
|
|
454
|
-
const adoptResult =
|
|
639
|
+
const adoptResult = ctx.adoptSession(sessionID, parsed.data.cols, parsed.data.rows);
|
|
455
640
|
|
|
456
641
|
if (adoptResult === 'ok') {
|
|
457
642
|
this.sendOk(req.id, {});
|
|
@@ -472,7 +657,7 @@ export class DaemonConnection {
|
|
|
472
657
|
return;
|
|
473
658
|
}
|
|
474
659
|
case 'session.attach': {
|
|
475
|
-
this.applyAttach(req);
|
|
660
|
+
this.applyAttach(req, ctx);
|
|
476
661
|
|
|
477
662
|
return;
|
|
478
663
|
}
|
|
@@ -485,43 +670,43 @@ export class DaemonConnection {
|
|
|
485
670
|
return;
|
|
486
671
|
}
|
|
487
672
|
|
|
488
|
-
|
|
673
|
+
ctx.detachSession(this, parsed.data.session);
|
|
489
674
|
this.sendOk(req.id, {});
|
|
490
675
|
|
|
491
676
|
return;
|
|
492
677
|
}
|
|
493
678
|
case 'session.input': {
|
|
494
|
-
this.applyInput(req);
|
|
679
|
+
this.applyInput(req, ctx);
|
|
495
680
|
|
|
496
681
|
return;
|
|
497
682
|
}
|
|
498
683
|
case 'session.resize': {
|
|
499
|
-
this.applyResize(req);
|
|
684
|
+
this.applyResize(req, ctx);
|
|
500
685
|
|
|
501
686
|
return;
|
|
502
687
|
}
|
|
503
688
|
case 'permission.respond': {
|
|
504
|
-
this.applyPermissionRespond(req);
|
|
689
|
+
this.applyPermissionRespond(req, ctx);
|
|
505
690
|
|
|
506
691
|
return;
|
|
507
692
|
}
|
|
508
693
|
case 'session.message': {
|
|
509
|
-
await this.applySessionMessage(req);
|
|
694
|
+
await this.applySessionMessage(req, ctx);
|
|
510
695
|
|
|
511
696
|
return;
|
|
512
697
|
}
|
|
513
698
|
case 'session.tap': {
|
|
514
|
-
this.applyTap(req);
|
|
699
|
+
this.applyTap(req, ctx);
|
|
515
700
|
|
|
516
701
|
return;
|
|
517
702
|
}
|
|
518
703
|
case 'message.get': {
|
|
519
|
-
await this.applyMessageGet(req);
|
|
704
|
+
await this.applyMessageGet(req, ctx);
|
|
520
705
|
|
|
521
706
|
return;
|
|
522
707
|
}
|
|
523
708
|
case 'message.ack': {
|
|
524
|
-
await this.applyMessageAck(req);
|
|
709
|
+
await this.applyMessageAck(req, ctx);
|
|
525
710
|
|
|
526
711
|
return;
|
|
527
712
|
}
|
|
@@ -535,23 +720,23 @@ export class DaemonConnection {
|
|
|
535
720
|
}
|
|
536
721
|
|
|
537
722
|
this.sendOk(req.id, {
|
|
538
|
-
restored: await
|
|
723
|
+
restored: await ctx.restoreFleet(parsed.data.cols, parsed.data.rows),
|
|
539
724
|
});
|
|
540
725
|
|
|
541
726
|
return;
|
|
542
727
|
}
|
|
543
728
|
case 'session.get': {
|
|
544
|
-
await this.applySessionGet(req);
|
|
729
|
+
await this.applySessionGet(req, ctx);
|
|
545
730
|
|
|
546
731
|
return;
|
|
547
732
|
}
|
|
548
733
|
case 'session.read': {
|
|
549
|
-
await this.applySessionRead(req);
|
|
734
|
+
await this.applySessionRead(req, ctx);
|
|
550
735
|
|
|
551
736
|
return;
|
|
552
737
|
}
|
|
553
738
|
case 'events.read': {
|
|
554
|
-
await this.applyEventsRead(req);
|
|
739
|
+
await this.applyEventsRead(req, ctx);
|
|
555
740
|
|
|
556
741
|
return;
|
|
557
742
|
}
|
|
@@ -561,7 +746,7 @@ export class DaemonConnection {
|
|
|
561
746
|
}
|
|
562
747
|
}
|
|
563
748
|
|
|
564
|
-
private applySpawn(req: RequestMsg): void {
|
|
749
|
+
private async applySpawn(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
565
750
|
const parsed = parseRequestParams('session.spawn', req.p);
|
|
566
751
|
|
|
567
752
|
if (!parsed.ok) {
|
|
@@ -570,48 +755,78 @@ export class DaemonConnection {
|
|
|
570
755
|
return;
|
|
571
756
|
}
|
|
572
757
|
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
758
|
+
const data = parsed.data;
|
|
759
|
+
|
|
760
|
+
// Every refusal is thrown from the plan, which runs only once a key is
|
|
761
|
+
// claimed: a retry of a held key answers from the key without checking
|
|
762
|
+
// anything again, and a refused spawn drops its claim.
|
|
763
|
+
const plan = (): SpawnParams => {
|
|
764
|
+
// The target goes first: a config or target problem is the cause a
|
|
765
|
+
// spawn reports, ahead of any agent check that problem can skew.
|
|
766
|
+
const target = ctx.resolveSpawnTarget(data.target);
|
|
767
|
+
const agent: AgentID = data.agent ?? 'claude';
|
|
768
|
+
const adapter = ctx.findAdapter(agent);
|
|
769
|
+
const entry = ctx.collectAgents().agents.find((candidate) => candidate.id === agent);
|
|
770
|
+
|
|
771
|
+
if (adapter === null || entry === undefined) {
|
|
772
|
+
throw new DaemonError('unsupported', `no adapter for agent '${agent}'`);
|
|
773
|
+
}
|
|
579
774
|
|
|
580
|
-
|
|
581
|
-
|
|
775
|
+
// A stand-in adapter declares no binary to check, so only a profiled
|
|
776
|
+
// agent's missing binary refuses the spawn.
|
|
777
|
+
if (adapter.profile !== undefined && !entry.installed) {
|
|
778
|
+
throw new DaemonError(
|
|
779
|
+
'unsupported',
|
|
780
|
+
`agent '${agent}' is registered but not installed on this host`,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
582
783
|
|
|
583
|
-
|
|
784
|
+
const overrides = parseSpawnOverrides(entry, { model: data.model, effort: data.effort });
|
|
584
785
|
|
|
585
|
-
|
|
586
|
-
|
|
786
|
+
if (!overrides.ok) {
|
|
787
|
+
throw new DaemonError(overrides.code, overrides.message);
|
|
788
|
+
}
|
|
587
789
|
|
|
588
|
-
|
|
589
|
-
this.sendErr(req.id, 'no_such_session', `no session '${parsed.data.parent}'`);
|
|
790
|
+
let parent: SessionID | null = null;
|
|
590
791
|
|
|
591
|
-
|
|
592
|
-
|
|
792
|
+
if (data.parent !== undefined) {
|
|
793
|
+
const owner = ctx.collectSessions().find((s) => s.id === data.parent);
|
|
593
794
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}
|
|
795
|
+
if (owner === undefined) {
|
|
796
|
+
throw new DaemonError('no_such_session', `no session '${data.parent}'`);
|
|
797
|
+
}
|
|
598
798
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
cols: parsed.data.cols,
|
|
604
|
-
rows: parsed.data.rows,
|
|
605
|
-
resume: parsed.data.resume,
|
|
606
|
-
namedBy: name === '' ? 'auto' : 'user',
|
|
607
|
-
agent,
|
|
608
|
-
parent,
|
|
609
|
-
});
|
|
799
|
+
// A sub-session spawning a sub-session of its own lands beside it,
|
|
800
|
+
// so a set stays one level deep.
|
|
801
|
+
parent = owner.parent ?? owner.id;
|
|
802
|
+
}
|
|
610
803
|
|
|
611
|
-
|
|
804
|
+
return {
|
|
805
|
+
cwd: data.cwd,
|
|
806
|
+
name: data.name === '' ? basename(data.cwd) : data.name,
|
|
807
|
+
prompt: data.prompt,
|
|
808
|
+
cols: data.cols,
|
|
809
|
+
rows: data.rows,
|
|
810
|
+
resume: data.resume,
|
|
811
|
+
namedBy: data.name === '' ? 'auto' : 'user',
|
|
812
|
+
agent,
|
|
813
|
+
parent,
|
|
814
|
+
overrides: overrides.overrides,
|
|
815
|
+
target,
|
|
816
|
+
};
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
const keyed =
|
|
820
|
+
data.idempotencyKey === undefined
|
|
821
|
+
? null
|
|
822
|
+
: { key: data.idempotencyKey, payloadHash: buildPayloadHash(data) };
|
|
823
|
+
|
|
824
|
+
const spawned = await ctx.spawnSession(plan, keyed, null);
|
|
825
|
+
|
|
826
|
+
this.sendOk(req.id, spawned);
|
|
612
827
|
}
|
|
613
828
|
|
|
614
|
-
private applyAttach(req: RequestMsg): void {
|
|
829
|
+
private applyAttach(req: RequestMsg, ctx: DaemonContext): void {
|
|
615
830
|
const parsed = parseRequestParams('session.attach', req.p);
|
|
616
831
|
|
|
617
832
|
if (!parsed.ok) {
|
|
@@ -622,7 +837,7 @@ export class DaemonConnection {
|
|
|
622
837
|
|
|
623
838
|
const sessionID = parsed.data.session;
|
|
624
839
|
|
|
625
|
-
const result =
|
|
840
|
+
const result = ctx.attachSession(this, sessionID, {
|
|
626
841
|
cols: parsed.data.cols,
|
|
627
842
|
rows: parsed.data.rows,
|
|
628
843
|
});
|
|
@@ -639,12 +854,12 @@ export class DaemonConnection {
|
|
|
639
854
|
return;
|
|
640
855
|
}
|
|
641
856
|
|
|
642
|
-
const dims =
|
|
857
|
+
const dims = ctx.getEffectiveDims(sessionID);
|
|
643
858
|
|
|
644
859
|
this.sendOk(req.id, { cols: dims.cols, rows: dims.rows });
|
|
645
860
|
}
|
|
646
861
|
|
|
647
|
-
private applyInput(req: RequestMsg): void {
|
|
862
|
+
private applyInput(req: RequestMsg, ctx: DaemonContext): void {
|
|
648
863
|
const parsed = parseRequestParams('session.input', req.p);
|
|
649
864
|
|
|
650
865
|
if (!parsed.ok) {
|
|
@@ -654,7 +869,7 @@ export class DaemonConnection {
|
|
|
654
869
|
}
|
|
655
870
|
|
|
656
871
|
const sessionID = parsed.data.session;
|
|
657
|
-
const result =
|
|
872
|
+
const result = ctx.writeSessionInput(sessionID, parsed.data.d);
|
|
658
873
|
|
|
659
874
|
if (result === 'missing') {
|
|
660
875
|
this.sendErr(req.id, 'no_such_session', `no session '${sessionID}'`);
|
|
@@ -681,7 +896,7 @@ export class DaemonConnection {
|
|
|
681
896
|
this.sendOk(req.id, {});
|
|
682
897
|
}
|
|
683
898
|
|
|
684
|
-
private applyResize(req: RequestMsg): void {
|
|
899
|
+
private applyResize(req: RequestMsg, ctx: DaemonContext): void {
|
|
685
900
|
const parsed = parseRequestParams('session.resize', req.p);
|
|
686
901
|
|
|
687
902
|
if (!parsed.ok) {
|
|
@@ -692,9 +907,7 @@ export class DaemonConnection {
|
|
|
692
907
|
|
|
693
908
|
const sessionID = parsed.data.session;
|
|
694
909
|
|
|
695
|
-
if (
|
|
696
|
-
!this.ctx.resizeSession(this, sessionID, { cols: parsed.data.cols, rows: parsed.data.rows })
|
|
697
|
-
) {
|
|
910
|
+
if (!ctx.resizeSession(this, sessionID, { cols: parsed.data.cols, rows: parsed.data.rows })) {
|
|
698
911
|
this.sendErr(req.id, 'bad_args', `not attached to session '${sessionID}'`);
|
|
699
912
|
|
|
700
913
|
return;
|
|
@@ -703,7 +916,7 @@ export class DaemonConnection {
|
|
|
703
916
|
this.sendOk(req.id, {});
|
|
704
917
|
}
|
|
705
918
|
|
|
706
|
-
private applyPermissionRespond(req: RequestMsg): void {
|
|
919
|
+
private applyPermissionRespond(req: RequestMsg, ctx: DaemonContext): void {
|
|
707
920
|
const parsed = parseRequestParams('permission.respond', req.p);
|
|
708
921
|
|
|
709
922
|
if (!parsed.ok) {
|
|
@@ -714,7 +927,7 @@ export class DaemonConnection {
|
|
|
714
927
|
|
|
715
928
|
const request = parsed.data.request;
|
|
716
929
|
const decision = parsed.data.decision;
|
|
717
|
-
const result =
|
|
930
|
+
const result = ctx.answerPermission(request, decision);
|
|
718
931
|
|
|
719
932
|
switch (result) {
|
|
720
933
|
case 'ok': {
|
|
@@ -738,7 +951,7 @@ export class DaemonConnection {
|
|
|
738
951
|
}
|
|
739
952
|
}
|
|
740
953
|
|
|
741
|
-
private async applySessionGet(req: RequestMsg): Promise<void> {
|
|
954
|
+
private async applySessionGet(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
742
955
|
const parsed = parseRequestParams('session.get', req.p);
|
|
743
956
|
|
|
744
957
|
if (!parsed.ok) {
|
|
@@ -749,7 +962,7 @@ export class DaemonConnection {
|
|
|
749
962
|
|
|
750
963
|
const id = parsed.data.session;
|
|
751
964
|
|
|
752
|
-
const record = await
|
|
965
|
+
const record = await ctx.readSessionRecord(id);
|
|
753
966
|
|
|
754
967
|
if (record === 'missing') {
|
|
755
968
|
this.sendErr(req.id, 'no_such_session', `no session '${id}'`);
|
|
@@ -760,7 +973,7 @@ export class DaemonConnection {
|
|
|
760
973
|
this.sendOk(req.id, { ...record });
|
|
761
974
|
}
|
|
762
975
|
|
|
763
|
-
private async applySessionRead(req: RequestMsg): Promise<void> {
|
|
976
|
+
private async applySessionRead(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
764
977
|
const parsed = parseRequestParams('session.read', req.p);
|
|
765
978
|
|
|
766
979
|
if (!parsed.ok) {
|
|
@@ -784,7 +997,7 @@ export class DaemonConnection {
|
|
|
784
997
|
from = { path: decoded.path, offset: decoded.offset };
|
|
785
998
|
}
|
|
786
999
|
|
|
787
|
-
const read = await
|
|
1000
|
+
const read = await ctx.loadSessionTranscript(id, from, parsed.data.limit);
|
|
788
1001
|
|
|
789
1002
|
if (read === 'missing') {
|
|
790
1003
|
this.sendErr(req.id, 'no_such_session', `no session '${id}'`);
|
|
@@ -803,7 +1016,7 @@ export class DaemonConnection {
|
|
|
803
1016
|
}
|
|
804
1017
|
}
|
|
805
1018
|
|
|
806
|
-
private async applyEventsRead(req: RequestMsg): Promise<void> {
|
|
1019
|
+
private async applyEventsRead(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
807
1020
|
const parsed = parseRequestParams('events.read', req.p);
|
|
808
1021
|
|
|
809
1022
|
if (!parsed.ok) {
|
|
@@ -826,18 +1039,25 @@ export class DaemonConnection {
|
|
|
826
1039
|
afterID = decoded.id;
|
|
827
1040
|
}
|
|
828
1041
|
|
|
829
|
-
const
|
|
1042
|
+
const page = await ctx.readEvents(
|
|
1043
|
+
afterID,
|
|
1044
|
+
parsed.data.limit,
|
|
1045
|
+
parsed.data.waitMs,
|
|
1046
|
+
parsed.data.session ?? null,
|
|
1047
|
+
null,
|
|
1048
|
+
);
|
|
830
1049
|
|
|
831
|
-
const last = events.at(-1);
|
|
1050
|
+
const last = page.events.at(-1);
|
|
832
1051
|
|
|
833
1052
|
// A cursor always comes back so a client can long-poll from an empty trail.
|
|
834
1053
|
this.sendOk(req.id, {
|
|
835
|
-
events,
|
|
1054
|
+
events: page.events,
|
|
836
1055
|
cursor: last === undefined ? encodeCursor({ kind: 'events', id: afterID ?? 0 }) : last.cursor,
|
|
1056
|
+
more: page.more,
|
|
837
1057
|
});
|
|
838
1058
|
}
|
|
839
1059
|
|
|
840
|
-
private async applySessionMessage(req: RequestMsg): Promise<void> {
|
|
1060
|
+
private async applySessionMessage(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
841
1061
|
const parsed = parseRequestParams('session.message', req.p);
|
|
842
1062
|
|
|
843
1063
|
if (!parsed.ok) {
|
|
@@ -848,10 +1068,16 @@ export class DaemonConnection {
|
|
|
848
1068
|
|
|
849
1069
|
const sessionID = parsed.data.session;
|
|
850
1070
|
|
|
851
|
-
const
|
|
1071
|
+
const keyed =
|
|
1072
|
+
parsed.data.idempotencyKey === undefined
|
|
1073
|
+
? null
|
|
1074
|
+
: { key: parsed.data.idempotencyKey, payloadHash: buildPayloadHash(parsed.data) };
|
|
1075
|
+
|
|
1076
|
+
const result = await ctx.writeSessionMessage(
|
|
852
1077
|
sessionID,
|
|
853
1078
|
parsed.data.from,
|
|
854
1079
|
parsed.data.text,
|
|
1080
|
+
keyed,
|
|
855
1081
|
);
|
|
856
1082
|
|
|
857
1083
|
if (result === 'missing') {
|
|
@@ -882,10 +1108,10 @@ export class DaemonConnection {
|
|
|
882
1108
|
return;
|
|
883
1109
|
}
|
|
884
1110
|
|
|
885
|
-
this.sendOk(req.id,
|
|
1111
|
+
this.sendOk(req.id, result);
|
|
886
1112
|
}
|
|
887
1113
|
|
|
888
|
-
private applyTap(req: RequestMsg): void {
|
|
1114
|
+
private applyTap(req: RequestMsg, ctx: DaemonContext): void {
|
|
889
1115
|
const parsed = parseRequestParams('session.tap', req.p);
|
|
890
1116
|
|
|
891
1117
|
if (!parsed.ok) {
|
|
@@ -895,7 +1121,7 @@ export class DaemonConnection {
|
|
|
895
1121
|
}
|
|
896
1122
|
|
|
897
1123
|
const sessionID = parsed.data.session;
|
|
898
|
-
const result =
|
|
1124
|
+
const result = ctx.attachTap(this, sessionID);
|
|
899
1125
|
|
|
900
1126
|
if (result === 'missing') {
|
|
901
1127
|
this.sendErr(req.id, 'no_such_session', `no session '${sessionID}'`);
|
|
@@ -912,7 +1138,7 @@ export class DaemonConnection {
|
|
|
912
1138
|
this.sendOk(req.id, {});
|
|
913
1139
|
}
|
|
914
1140
|
|
|
915
|
-
private async applyMessageGet(req: RequestMsg): Promise<void> {
|
|
1141
|
+
private async applyMessageGet(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
916
1142
|
const parsed = parseRequestParams('message.get', req.p);
|
|
917
1143
|
|
|
918
1144
|
if (!parsed.ok) {
|
|
@@ -923,7 +1149,7 @@ export class DaemonConnection {
|
|
|
923
1149
|
|
|
924
1150
|
const messageID = parsed.data.message;
|
|
925
1151
|
|
|
926
|
-
const view = await
|
|
1152
|
+
const view = await ctx.readMessage(messageID, parsed.data.waitMs);
|
|
927
1153
|
|
|
928
1154
|
if (view === null) {
|
|
929
1155
|
this.sendErr(req.id, 'bad_args', `no message '${messageID}'`);
|
|
@@ -943,10 +1169,12 @@ export class DaemonConnection {
|
|
|
943
1169
|
...(record.deliveredAt === undefined ? {} : { deliveredAt: record.deliveredAt }),
|
|
944
1170
|
...(record.answeredAt === undefined ? {} : { answeredAt: record.answeredAt }),
|
|
945
1171
|
...(record.answer === undefined ? {} : { answer: record.answer }),
|
|
1172
|
+
turn: record.turn ?? null,
|
|
1173
|
+
answeredWith: view.answeredWith,
|
|
946
1174
|
});
|
|
947
1175
|
}
|
|
948
1176
|
|
|
949
|
-
private async applyMessageAck(req: RequestMsg): Promise<void> {
|
|
1177
|
+
private async applyMessageAck(req: RequestMsg, ctx: DaemonContext): Promise<void> {
|
|
950
1178
|
const parsed = parseRequestParams('message.ack', req.p);
|
|
951
1179
|
|
|
952
1180
|
if (!parsed.ok) {
|
|
@@ -958,7 +1186,7 @@ export class DaemonConnection {
|
|
|
958
1186
|
const sessionID = parsed.data.session;
|
|
959
1187
|
const messageID = parsed.data.message;
|
|
960
1188
|
|
|
961
|
-
const result = await
|
|
1189
|
+
const result = await ctx.ackMessage(this, sessionID, messageID);
|
|
962
1190
|
|
|
963
1191
|
if (result === 'not_tapping') {
|
|
964
1192
|
this.sendErr(req.id, 'bad_args', `this connection is not tapping session '${sessionID}'`);
|
|
@@ -1003,6 +1231,21 @@ export class DaemonConnection {
|
|
|
1003
1231
|
const parsedHello = parseRequestParams('daemon.hello', req.p);
|
|
1004
1232
|
const client = parsedHello.ok ? parsedHello.data.client : 'unknown client';
|
|
1005
1233
|
|
|
1234
|
+
// A principal the handshake cannot read would otherwise leave the
|
|
1235
|
+
// connection with the owner's reach.
|
|
1236
|
+
if (!parsedHello.ok && req.p?.['principal'] !== undefined) {
|
|
1237
|
+
this.sendErr(req.id, 'bad_args', parsedHello.message);
|
|
1238
|
+
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const principal = parsedHello.ok ? (parsedHello.data.principal ?? null) : null;
|
|
1243
|
+
|
|
1244
|
+
if (principal !== null) {
|
|
1245
|
+
this.principal = principal;
|
|
1246
|
+
this.access = this.ctx.buildTargetAccess(principal);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1006
1249
|
if (req.v !== PROTOCOL_V) {
|
|
1007
1250
|
this.sendErr(
|
|
1008
1251
|
req.id,
|
|
@@ -1024,7 +1267,9 @@ export class DaemonConnection {
|
|
|
1024
1267
|
private async sendHelloOk(id: number): Promise<void> {
|
|
1025
1268
|
this.sendOk(id, {
|
|
1026
1269
|
daemon: this.ctx.build,
|
|
1270
|
+
daemonID: this.ctx.daemonID,
|
|
1027
1271
|
limits: { maxLine: MAX_LINE, maxChunk: MAX_CHUNK },
|
|
1272
|
+
features: DAEMON_FEATURES,
|
|
1028
1273
|
lastUsedAgent: await this.ctx.loadLastUsedAgent(),
|
|
1029
1274
|
});
|
|
1030
1275
|
}
|
|
@@ -1033,7 +1278,34 @@ export class DaemonConnection {
|
|
|
1033
1278
|
this.queue.send(encodeMessage({ v: PROTOCOL_V, id, ok }));
|
|
1034
1279
|
}
|
|
1035
1280
|
|
|
1036
|
-
private sendErr(
|
|
1037
|
-
|
|
1281
|
+
private sendErr(
|
|
1282
|
+
id: number,
|
|
1283
|
+
code: ErrorCode,
|
|
1284
|
+
msg: string,
|
|
1285
|
+
data?: Readonly<Record<string, unknown>>,
|
|
1286
|
+
): void {
|
|
1287
|
+
this.queue.send(
|
|
1288
|
+
encodeMessage({
|
|
1289
|
+
v: PROTOCOL_V,
|
|
1290
|
+
id,
|
|
1291
|
+
err: { code, msg, ...(data === undefined ? {} : { data }) },
|
|
1292
|
+
}),
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// The session an event belongs to, from its session id or the session it
|
|
1298
|
+
// adds, or null for an event of no session.
|
|
1299
|
+
function findEventSession(event: EventMsg): SessionID | null {
|
|
1300
|
+
if (typeof event['s'] === 'string') {
|
|
1301
|
+
return toSessionID(event['s']);
|
|
1038
1302
|
}
|
|
1303
|
+
|
|
1304
|
+
const session = event['session'];
|
|
1305
|
+
|
|
1306
|
+
if (isRecord(session) && typeof session['id'] === 'string') {
|
|
1307
|
+
return toSessionID(session['id']);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
return null;
|
|
1039
1311
|
}
|