1// @effect-diagnostics-next-line nodeBuiltinImport:off - The HTTPS test server needs certificate bytes.
2import * as NodeFS from "node:fs";
3// @effect-diagnostics-next-line nodeBuiltinImport:off - A peer with a certificate this machine does not trust.
4import * as NodeHttps from "node:https";
5import { NodeHttpServer } from "@effect/platform-node";
6import { type ExecutionEnvironmentDescriptor, OrchestratorMcpFailure } from "@t3tools/contracts";
7import { expect, it } from "@effect/vitest";
8import * as Context from "effect/Context";
9import * as Deferred from "effect/Deferred";
10import * as Duration from "effect/Duration";
11import * as Effect from "effect/Effect";
12import * as Fiber from "effect/Fiber";
13import * as Layer from "effect/Layer";
14import * as Option from "effect/Option";
15import * as Queue from "effect/Queue";
16import * as Ref from "effect/Ref";
17import * as Schema from "effect/Schema";
18import * as HttpRouter from "effect/http/HttpRouter";
19import * as SqlClient from "effect/sql/SqlClient";
20import * as TestClock from "effect/testing/TestClock";
21import { Tool, Toolkit } from "effect/ai";
22import {
23 FetchHttpClient,
24 HttpClient,
25 HttpServer,
26 HttpServerRequest,
27 HttpServerResponse,
28} from "effect/http";
29
30import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
31import * as McpHttpServer from "../mcp/McpHttpServer.ts";
32import * as McpInvocationContext from "../mcp/McpInvocationContext.ts";
33import * as McpToolAccess from "../mcp/McpToolAccess.ts";
34import * as PeerLinks from "./PeerLinks.ts";
35import * as ServerSettings from "../serverSettings.ts";
36import {
37 descriptorOf,
38 layerLinkingEnvironment,
39 servePeer,
40 type ServedPeer,
41} from "./PeerLinks.testkit.ts";
42import * as PeerMcpClient from "./PeerMcpClient.ts";
43
44import { layerReaderDatabase, traceSqlRouting } from "../persistence/testkit/SqliteRouting.ts";
45
46/** What B's probe saw: the calling client and the modes it may start work with. */
47const Probe = Schema.Struct({ caller: Schema.String, modes: Schema.String });
48
49const ProbeToolkit = Toolkit.make(
50 Tool.make("probe", {
51 description: "Reports the calling client and the modes it may start work with.",
52 success: Probe,
53 failure: OrchestratorMcpFailure,
54 failureMode: "return",
55 dependencies: [McpInvocationContext.McpInvocationContext],
56 }),
57 Tool.make("refuse", {
58 description: "Always refuses.",
59 success: Probe,
60 failure: OrchestratorMcpFailure,
61 failureMode: "return",
62 }),
63);
64
65const probeHandlers = McpToolAccess.toLayer(ProbeToolkit, {
66 probe: McpToolAccess.reads(() =>
67 McpInvocationContext.McpInvocationContext.pipe(
68 Effect.map((scope) => {
69 const modes = McpInvocationContext.clientModeCeiling(scope.client);
70 return {
71 caller: scope.client?.label ?? "none",
72 modes: `${modes.runtimeMode}/${modes.interactionMode}`,
73 };
74 }),
75 ),
76 ),
77 refuse: McpToolAccess.reads(() =>
78 Effect.fail(
79 new OrchestratorMcpFailure({ code: "thread_not_found", message: "No such thread." }),
80 ),
81 ),
82});
83
84const serveB = (descriptor: ExecutionEnvironmentDescriptor) =>
85 servePeer(descriptor, McpHttpServer.toolkitRegistration(ProbeToolkit, probeHandlers));
86
87/** Environment A, which links to B. */
88const makeA = (
89 descriptor: ExecutionEnvironmentDescriptor,
90 secrets?: typeof ServerSecretStore.layer,
91) =>
92 layerLinkingEnvironment(descriptor, secrets).pipe(
93 Layer.build,
94 Effect.map((context) => ({
95 links: Context.get(context, PeerLinks.PeerLinks),
96 peer: Context.get(context, PeerMcpClient.PeerMcpClient),
97 sql: Context.get(context, SqlClient.SqlClient),
98 settings: Context.get(context, ServerSettings.ServerSettingsService),
99 })),
100 );
101
102const laptop = descriptorOf("environment-laptop", "Laptop");
103const box = descriptorOf("environment-box", "Box");
104const probe = (
105 a: Effect.Success<ReturnType<typeof makeA>>,
106 limits: PeerMcpClient.PeerCallLimits = { runtimeMode: "full-access", interactionMode: "default" },
107 tool = "probe",
108) =>
109 a.peer.call({
110 environmentId: box.environmentId,
111 tool,
112 arguments: {},
113 limits,
114 success: Probe,
115 });
116
117const linkedSessions = (b: ServedPeer) => b.linkedSessions;
118
119/** Another environment on a socket of its own, answering as `descriptor`. */
120const serveApart = (descriptor: ExecutionEnvironmentDescriptor) =>
121 Layer.build(Layer.fresh(NodeHttpServer.layerTest)).pipe(
122 Effect.flatMap((server) => serveB(descriptor).pipe(Effect.provideContext(server))),
123 );
124
125it.effect("routes peer reads to readers and sees uncommitted link updates", () =>
126 Effect.scoped(
127 Effect.gen(function* () {
128 const context = yield* layerLinkingEnvironment(
129 laptop,
130 ServerSecretStore.layer,
131 layerReaderDatabase,
132 ).pipe(Layer.build);
133 const links = Context.get(context, PeerLinks.PeerLinks);
134 const sql = Context.get(context, SqlClient.SqlClient);
135 yield* sql`
136 INSERT INTO peer_environment_links (
137 environment_id, secret_name, label, urls_json, access, linked_at, expires_at, fenced
138 ) VALUES (
139 ${box.environmentId}, 'test-secret', 'Box', '[]', 'auto',
140 '2026-01-01T00:00:00.000Z', '2026-01-02T00:00:00.000Z', 1
141 )
142 `;
143 const { queries, trace } = traceSqlRouting();
144 yield* links.list.pipe(trace);
145 expect(Option.getOrThrow(yield* links.get(box.environmentId).pipe(trace)).label).toBe("Box");
146 const reads = queries.filter(({ query }) =>
147 query.includes("SELECT * FROM peer_environment_links"),
148 );
149 expect(reads).toHaveLength(2);
150 expect(reads.map((row) => row.connection)).toEqual(["reader", "reader"]);
151 yield* sql.withTransaction(
152 Effect.gen(function* () {
153 yield* sql`UPDATE peer_environment_links SET label = 'Uncommitted box'`;
154 expect(Option.getOrThrow(yield* links.get(box.environmentId)).label).toBe(
155 "Uncommitted box",
156 );
157 expect((yield* links.list)[0]?.label).toBe("Uncommitted box");
158 }),
159 );
160 expect(yield* links.unlink(box.environmentId)).toBe(true);
161 expect(yield* links.list).toEqual([]);
162 }),
163 ),
164);
165
166it.effect("links with a pairing code, and the peer holds its agents to the limits they carry", () =>
167 Effect.scoped(
168 Effect.gen(function* () {
169 const b = yield* serveB(box);
170 const a = yield* makeA(laptop);
171
172 const refused = yield* a.links
173 .link({ url: b.url, pairingCode: "not-a-code", access: "auto" })
174 .pipe(Effect.flip);
175 expect(refused.reason).toBe("pairing_rejected");
176 expect(yield* linkedSessions(b)).toEqual([]);
177
178 const pairing = yield* b.auth.issuePairingCredential();
179 const linked = yield* a.links.link({
180 url: b.url,
181 pairingCode: pairing.credential,
182 access: "auto",
183 });
184 expect(linked).toMatchObject({
185 environmentId: box.environmentId,
186 label: "Box",
187 urls: [b.url],
188 access: "auto",
189 });
190 // B lists the link under A's name, where it can be revoked.
191 expect((yield* linkedSessions(b)).map((session) => session.client.label)).toEqual([
192 "T3 Code · Laptop",
193 ]);
194 expect(
195 (yield* a.links.list).map(({ environmentId, status }) => [environmentId, status]),
196 ).toEqual([[box.environmentId, "reachable"]]);
197
198 // The caller's limits narrow the link's access; they never widen it.
199 expect(
200 yield* probe(a, { runtimeMode: "approval-required", interactionMode: "plan" }),
201 ).toEqual({ caller: "T3 Code · Laptop", modes: "approval-required/plan" });
202 expect(yield* probe(a)).toEqual({ caller: "T3 Code · Laptop", modes: "auto/default" });
203
204 // A refusal keeps the peer's reason.
205 const notFound = yield* probe(a, undefined, "refuse").pipe(Effect.flip);
206 expect(notFound).toMatchObject({ code: "thread_not_found", message: "No such thread." });
207 }),
208 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
209);
210
211it.effect(
212 "each machine's Linked environments beta gates links, and a link waits while it is off",
213 () =>
214 Effect.scoped(
215 Effect.gen(function* () {
216 const b = yield* serveB(box);
217 const a = yield* makeA(laptop);
218 const pairing = yield* b.auth.issuePairingCredential();
219 const link = a.links.link({ url: b.url, pairingCode: pairing.credential, access: "auto" });
220
221 yield* a.settings.updateSettings({ linkedEnvironmentsEnabled: false });
222 expect(yield* link.pipe(Effect.flip)).toMatchObject({
223 reason: "disabled",
224 message: PeerLinks.LINKED_ENVIRONMENTS_OFF,
225 });
226 expect(yield* Ref.get(b.bearers)).toEqual([]);
227
228 yield* a.settings.updateSettings({ linkedEnvironmentsEnabled: true });
229 yield* b.settings.updateSettings({ linkedEnvironmentsEnabled: false });
230 const refusedThere = yield* link.pipe(Effect.flip);
231 expect(refusedThere.reason).toBe("pairing_rejected");
232 expect(refusedThere.message).toContain("Linked environments is off on that machine");
233 expect(yield* linkedSessions(b)).toEqual([]);
234
235 yield* b.settings.updateSettings({ linkedEnvironmentsEnabled: true });
236 yield* link;
237 yield* b.settings.updateSettings({ linkedEnvironmentsEnabled: false });
238 // Not final, so a task already running there picks up again once it is back on.
239 const waiting = yield* probe(a).pipe(Effect.flip);
240 expect(waiting.code).toBe("orchestration_error");
241 expect(waiting.message).toContain("has Linked environments turned off");
242 yield* b.settings.updateSettings({ linkedEnvironmentsEnabled: true });
243 expect(yield* probe(a)).toEqual({ caller: "T3 Code · Laptop", modes: "auto/default" });
244 }),
245 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
246);
247
248it.effect("a link whose row cannot be stored keeps none of its token", () =>
249 Effect.scoped(
250 Effect.gen(function* () {
251 const b = yield* serveB(box);
252 const held = yield* Ref.make<ReadonlySet<string>>(new Set());
253 const noted = Layer.effect(
254 ServerSecretStore.ServerSecretStore,
255 Effect.map(ServerSecretStore.make, (store) =>
256 ServerSecretStore.ServerSecretStore.of({
257 ...store,
258 set: (name, value) =>
259 store
260 .set(name, value)
261 .pipe(Effect.andThen(Ref.update(held, (names) => new Set([...names, name])))),
262 remove: (name) =>
263 store
264 .remove(name)
265 .pipe(
266 Effect.andThen(
267 Ref.update(held, (names) => new Set([...names].filter((at) => at !== name))),
268 ),
269 ),
270 }),
271 ),
272 );
273 const a = yield* makeA(laptop, noted);
274 yield* a.sql`
275 CREATE TRIGGER refuse_link BEFORE INSERT ON peer_environment_links
276 BEGIN SELECT RAISE(ABORT, 'database or disk is full'); END
277 `;
278 const pairing = yield* b.auth.issuePairingCredential();
279 const failed = yield* a.links
280 .link({ url: b.url, pairingCode: pairing.credential, access: "auto" })
281 .pipe(Effect.flip);
282 expect(failed.reason).toBe("storage");
283 expect(yield* Ref.get(held)).toEqual(new Set());
284 }),
285 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
286);
287
288it.effect("two links racing to one peer leave the stored access matching the token", () =>
289 Effect.scoped(
290 Effect.gen(function* () {
291 const b = yield* serveB(box);
292 const a = yield* makeA(laptop);
293 const [first, second] = yield* Effect.all(
294 [b.auth.issuePairingCredential(), b.auth.issuePairingCredential()],
295 { concurrency: 2 },
296 );
297 yield* Effect.all(
298 [
299 a.links.link({ url: b.url, pairingCode: first.credential, access: "read-only" }),
300 a.links.link({ url: b.url, pairingCode: second.credential, access: "full-access" }),
301 ],
302 { concurrency: 2 },
303 );
304 const [stored] = yield* a.links.list;
305 // The token sent is the one issued with the access the row records.
306 const { modes } = yield* probe(a);
307 expect(modes).toBe(
308 stored!.access === "read-only" ? "approval-required/default" : "full-access/default",
309 );
310 }),
311 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
312);
313
314/** A bare HTTP server on a socket of its own, answering with `handle`. */
315const serveRaw = <E, R>(
316 handle: (
317 request: HttpServerRequest.HttpServerRequest,
318 ) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>,
319) =>
320 Layer.build(Layer.fresh(NodeHttpServer.layerTest)).pipe(
321 Effect.flatMap((server) =>
322 Effect.gen(function* () {
323 yield* HttpServer.serve(Effect.flatMap(HttpServerRequest.HttpServerRequest, handle)).pipe(
324 Layer.build,
325 );
326 const address = (yield* HttpServer.HttpServer).address;
327 if (address._tag === "UnixPathAddress") return yield* Effect.die("expected a TCP address");
328 return `http://127.0.0.1:${address.port}`;
329 }).pipe(Effect.provideContext(server)),
330 ),
331 );
332
333it.effect("never follows a peer's redirect with the pairing code", () =>
334 Effect.scoped(
335 Effect.gen(function* () {
336 const b = yield* serveB(box);
337 const received = yield* Ref.make<ReadonlyArray<string>>([]);
338 const elsewhere = yield* serveRaw((request) =>
339 Ref.update(received, (seen) => [...seen, `${request.method} ${request.url}`]).pipe(
340 Effect.as(HttpServerResponse.empty({ status: 404 })),
341 ),
342 );
343 // Answers as B, then sends every linking request on to another address.
344 const front = yield* serveRaw((request) =>
345 request.url.startsWith("/.well-known/t3/environment")
346 ? HttpClient.get(`${b.url}${request.url}`).pipe(
347 Effect.flatMap((response) => response.text),
348 Effect.map((body) =>
349 HttpServerResponse.text(body, { contentType: "application/json" }),
350 ),
351 Effect.provide(FetchHttpClient.layer),
352 Effect.orDie,
353 )
354 : Effect.succeed(
355 HttpServerResponse.empty({
356 status: 307,
357 headers: { location: `${elsewhere}${request.url}` },
358 }),
359 ),
360 );
361 const a = yield* makeA(laptop);
362 const pairing = yield* b.auth.issuePairingCredential();
363 yield* a.links
364 .link({ url: front, pairingCode: pairing.credential, access: "auto" })
365 .pipe(Effect.flip);
366 expect(yield* Ref.get(received)).toEqual([]);
367 expect(yield* a.links.list).toEqual([]);
368 }),
369 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
370);
371
372it.effect("never sends a link's token to an address that stops answering as that peer", () =>
373 Effect.scoped(
374 Effect.gen(function* () {
375 const b = yield* serveB(box);
376 const a = yield* makeA(laptop);
377 const pairing = yield* b.auth.issuePairingCredential();
378 yield* a.links.link({ url: b.url, pairingCode: pairing.credential, access: "auto" });
379 yield* probe(a);
380 expect((yield* Ref.get(b.bearers)).length).toBeGreaterThan(0);
381
382 // The address now answers as another environment, as a reused LAN address would.
383 yield* Ref.set(b.descriptor, descriptorOf("environment-impostor", "Impostor"));
384 yield* Ref.set(b.bearers, []);
385 const failed = yield* probe(a).pipe(Effect.flip);
386 expect(failed.message).toContain("Box did not answer");
387 expect(yield* Ref.get(b.bearers)).toEqual([]);
388 expect((yield* a.links.list).map((link) => link.status)).toEqual(["unreachable"]);
389 }),
390 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
391);
392
393it.effect("lists a peer asleep at every address as unreachable within one probe budget", () =>
394 Effect.scoped(
395 Effect.gen(function* () {
396 const b = yield* serveB(box);
397 // Another address of the same machine, asleep too.
398 const wake = yield* Deferred.make<void>();
399 const elsewhere = yield* serveRaw(() =>
400 Deferred.await(wake).pipe(Effect.as(HttpServerResponse.empty({ status: 503 }))),
401 );
402 yield* Effect.addFinalizer(() => Deferred.succeed(wake, undefined));
403 const a = yield* makeA(laptop);
404 const pairing = yield* b.auth.issuePairingCredential();
405 yield* a.links.link({
406 url: b.url,
407 alternateUrls: [elsewhere],
408 pairingCode: pairing.credential,
409 access: "auto",
410 });
411
412 yield* Ref.set(b.asleep, true);
413 const listing = yield* a.links.list.pipe(Effect.forkScoped);
414 yield* Queue.take(b.unanswered);
415 yield* TestClock.adjust(Duration.seconds(3));
416 const listed = yield* Fiber.join(listing);
417 expect(listed.map((link) => link.status)).toEqual(["unreachable"]);
418 }),
419 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
420);
421
422it.effect("lists a peer as reachable at another address while its first one sleeps", () =>
423 Effect.scoped(
424 Effect.gen(function* () {
425 const b = yield* serveB(box);
426 const described = yield* HttpClient.get(`${b.url}/.well-known/t3/environment`).pipe(
427 Effect.flatMap((response) => response.text),
428 Effect.provide(FetchHttpClient.layer),
429 );
430 // Another address of the same machine that answers.
431 const elsewhere = yield* serveRaw(() =>
432 Effect.succeed(HttpServerResponse.text(described, { contentType: "application/json" })),
433 );
434 const a = yield* makeA(laptop);
435 const pairing = yield* b.auth.issuePairingCredential();
436 yield* a.links.link({
437 url: b.url,
438 alternateUrls: [elsewhere],
439 pairingCode: pairing.credential,
440 access: "auto",
441 });
442
443 yield* Ref.set(b.asleep, true);
444 expect((yield* a.links.list).map((link) => link.status)).toEqual(["reachable"]);
445 }),
446 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
447);
448
449it.effect("cancelling a descriptor probe aborts its request and leaves later resolves fresh", () =>
450 Effect.scoped(
451 Effect.gen(function* () {
452 const hold = yield* Ref.make(false);
453 const requests = yield* Ref.make(0);
454 const entered = yield* Deferred.make<void>();
455 const interrupted = yield* Deferred.make<void>();
456 const holdDescriptor = HttpRouter.middleware(
457 (httpEffect) =>
458 Effect.gen(function* () {
459 const request = yield* HttpServerRequest.HttpServerRequest;
460 if (request.url.startsWith("/.well-known/t3/environment")) {
461 yield* Ref.update(requests, (count) => count + 1);
462 if (yield* Ref.get(hold)) {
463 return yield* Deferred.succeed(entered, undefined).pipe(
464 Effect.andThen(Effect.never),
465 Effect.onInterrupt(() =>
466 Deferred.succeed(interrupted, undefined).pipe(Effect.asVoid),
467 ),
468 Effect.interruptible,
469 );
470 }
471 }
472 return yield* httpEffect;
473 }),
474 { global: true },
475 );
476 const b = yield* servePeer(
477 box,
478 Layer.merge(McpHttpServer.toolkitRegistration(ProbeToolkit, probeHandlers), holdDescriptor),
479 );
480 const a = yield* makeA(laptop);
481 const pairing = yield* b.auth.issuePairingCredential();
482 yield* a.links.link({ url: b.url, pairingCode: pairing.credential, access: "auto" });
483 yield* Ref.set(b.bearers, []);
484 yield* Ref.set(requests, 0);
485 yield* Ref.set(hold, true);
486
487 const resolving = yield* a.links.resolve(box.environmentId).pipe(Effect.forkScoped);
488 yield* Deferred.await(entered);
489 yield* Fiber.interrupt(resolving);
490 yield* Deferred.await(interrupted);
491
492 yield* Ref.set(hold, false);
493 expect((yield* a.links.resolve(box.environmentId)).url).toBe(b.url);
494 expect(yield* Ref.get(requests)).toBe(2);
495 expect(yield* Ref.get(b.bearers)).toEqual([]);
496 }),
497 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
498);
499
500it.live("one dropped descriptor request does not list a live peer as unreachable", () =>
501 Effect.scoped(
502 Effect.gen(function* () {
503 const dropNext = yield* Ref.make(false);
504 const dropOnce = HttpRouter.middleware(
505 (httpEffect) =>
506 Effect.gen(function* () {
507 const request = yield* HttpServerRequest.HttpServerRequest;
508 if (
509 request.url.startsWith("/.well-known/t3/environment") &&
510 (yield* Ref.getAndSet(dropNext, false))
511 ) {
512 return HttpServerResponse.empty({ status: 503 });
513 }
514 return yield* httpEffect;
515 }),
516 { global: true },
517 );
518 const b = yield* servePeer(
519 box,
520 Layer.merge(McpHttpServer.toolkitRegistration(ProbeToolkit, probeHandlers), dropOnce),
521 );
522 const a = yield* makeA(laptop);
523 const pairing = yield* b.auth.issuePairingCredential();
524 yield* a.links.link({ url: b.url, pairingCode: pairing.credential, access: "auto" });
525
526 yield* Ref.set(dropNext, true);
527 expect((yield* a.links.list).map((link) => link.status)).toEqual(["reachable"]);
528 }),
529 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
530);
531
532it.effect("reads one stored link without asking the peer", () =>
533 Effect.scoped(
534 Effect.gen(function* () {
535 const b = yield* serveB(box);
536 const a = yield* makeA(laptop);
537 const pairing = yield* b.auth.issuePairingCredential();
538 yield* a.links.link({ url: b.url, pairingCode: pairing.credential, access: "auto" });
539 yield* Ref.set(b.probes, 0);
540
541 const stored = yield* a.links.get(box.environmentId);
542 expect(Option.map(stored, (link) => link.label)).toEqual(Option.some("Box"));
543 expect(Option.isNone(yield* a.links.get(laptop.environmentId))).toBe(true);
544 expect(yield* Ref.get(b.probes)).toBe(0);
545 }),
546 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
547);
548
549it.effect("a linked peer that is renamed shows its new name", () =>
550 Effect.scoped(
551 Effect.gen(function* () {
552 const b = yield* serveB(box);
553 const a = yield* makeA(laptop);
554 const pairing = yield* b.auth.issuePairingCredential();
555 yield* a.links.link({ url: b.url, pairingCode: pairing.credential, access: "auto" });
556
557 yield* Ref.set(b.descriptor, descriptorOf(box.environmentId, "Box Mini"));
558 expect((yield* a.links.resolve(box.environmentId)).link.label).toBe("Box Mini");
559 const stored = yield* a.links.get(box.environmentId);
560 expect(Option.map(stored, (link) => link.label)).toEqual(Option.some("Box Mini"));
561
562 yield* Ref.set(b.descriptor, descriptorOf(box.environmentId, "Box Studio"));
563 expect((yield* a.links.list).map((link) => link.label)).toEqual(["Box Studio"]);
564 const storedAfterList = yield* a.links.get(box.environmentId);
565 expect(Option.map(storedAfterList, (link) => link.label)).toEqual(Option.some("Box Studio"));
566 }),
567 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
568);
569
570it.effect("tells the agent to link again once the peer revokes the link or it expires", () =>
571 Effect.scoped(
572 Effect.gen(function* () {
573 const b = yield* serveB(box);
574 const a = yield* makeA(laptop);
575 const pairing = yield* b.auth.issuePairingCredential();
576 yield* a.links.link({ url: b.url, pairingCode: pairing.credential, access: "auto" });
577 yield* probe(a);
578
579 const [session] = yield* linkedSessions(b);
580 yield* b.auth.revokeSession(session!.sessionId);
581 const revoked = yield* probe(a).pipe(Effect.flip);
582 expect(revoked.code).toBe("capability_denied");
583 expect(revoked.message).toContain("revoked there");
584
585 yield* a.links.link({
586 url: b.url,
587 pairingCode: (yield* b.auth.issuePairingCredential()).credential,
588 access: "auto",
589 });
590 // A link made with a peer that later answers without the fence, as
591 // one made before the fence or a peer moved back to such a version.
592 yield* Ref.set(b.descriptor, descriptorOf("environment-box", "Box", { linkFence: false }));
593 yield* Ref.set(b.bearers, []);
594 const unfenced = yield* probe(a).pipe(Effect.flip);
595 expect(unfenced.code).toBe("capability_denied");
596 expect(unfenced.message).toContain("cannot keep a linked agent's limits");
597 expect(yield* Ref.get(b.bearers)).toEqual([]);
598 yield* Ref.set(b.descriptor, box);
599
600 // A link stored before links were marked: its session there is not a
601 // link's, so the peer would not fence its work even though it can.
602 yield* a.sql`UPDATE peer_environment_links SET fenced = 0`;
603 // Linking again is its only way back, as for an expired link.
604 expect((yield* a.links.list).map((link) => link.status)).toEqual(["expired"]);
605 const legacy = yield* probe(a).pipe(Effect.flip);
606 expect(legacy.code).toBe("capability_denied");
607 expect(legacy.message).toContain("Link it again");
608 expect(yield* Ref.get(b.bearers)).toEqual([]);
609 yield* a.links.link({
610 url: b.url,
611 pairingCode: (yield* b.auth.issuePairingCredential()).credential,
612 access: "auto",
613 });
614 yield* probe(a);
615
616 yield* TestClock.adjust(Duration.days(31));
617 expect((yield* a.links.list).map((link) => link.status)).toEqual(["expired"]);
618 yield* Ref.set(b.bearers, []);
619 const expired = yield* probe(a).pipe(Effect.flip);
620 expect(expired.code).toBe("capability_denied");
621 expect(expired.message).toContain("expired");
622 expect(yield* Ref.get(b.bearers)).toEqual([]);
623 }),
624 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
625);
626
627it.effect(
628 "refuses to link itself, plain http off this machine and the tailnet, or a peer that cannot hold a linked agent to its limits",
629 () =>
630 Effect.scoped(
631 Effect.gen(function* () {
632 const b = yield* serveB(
633 descriptorOf("environment-box", "Box", { mcpModeLimitHeader: false }),
634 );
635 const a = yield* makeA(laptop);
636 const pairing = yield* b.auth.issuePairingCredential();
637 const older = yield* a.links
638 .link({ url: b.url, pairingCode: pairing.credential, access: "auto" })
639 .pipe(Effect.flip);
640 expect(older.reason).toBe("incompatible");
641 // Nor one whose link's work could reach the user's own threads there.
642 yield* Ref.set(b.descriptor, descriptorOf("environment-box", "Box", { linkFence: false }));
643 const unfenced = yield* a.links
644 .link({ url: b.url, pairingCode: pairing.credential, access: "auto" })
645 .pipe(Effect.flip);
646 expect(unfenced.reason).toBe("incompatible");
647
648 // Plain http to a LAN address would carry the code and token in clear.
649 const lan = yield* a.links
650 .link({
651 url: "http://192.168.1.20:3773",
652 pairingCode: pairing.credential,
653 access: "auto",
654 })
655 .pipe(Effect.flip);
656 expect(lan.message).toContain("plain http");
657 const lanAlternate = yield* a.links
658 .link({
659 url: b.url,
660 alternateUrls: ["http://192.168.1.20:3773"],
661 pairingCode: pairing.credential,
662 access: "auto",
663 })
664 .pipe(Effect.flip);
665 expect(lanAlternate.message).toContain("plain http");
666
667 yield* Ref.set(b.descriptor, laptop);
668 const self = yield* a.links
669 .link({ url: b.url, pairingCode: pairing.credential, access: "auto" })
670 .pipe(Effect.flip);
671 expect(self.reason).toBe("self");
672 // Neither attempt spent the pairing code on a session.
673 expect(yield* linkedSessions(b)).toEqual([]);
674 }),
675 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
676);
677
678it.effect(
679 "linking a picked machine skips an address that answers as another, and never signs in there",
680 () =>
681 Effect.scoped(
682 Effect.gen(function* () {
683 const impostor = yield* serveApart(descriptorOf("environment-impostor", "Impostor"));
684 const b = yield* serveB(box);
685 const a = yield* makeA(laptop);
686 const pairing = yield* b.auth.issuePairingCredential();
687 const linked = yield* a.links.link({
688 url: impostor.url,
689 alternateUrls: [b.url],
690 pairingCode: pairing.credential,
691 access: "auto",
692 expectedEnvironmentId: box.environmentId,
693 });
694 expect(linked).toMatchObject({ environmentId: box.environmentId, urls: [b.url] });
695 // A picked machine is never linked at this machine's own loopback.
696 const own = yield* a.links
697 .link({
698 url: b.url.replace("127.0.0.1", "localhost"),
699 pairingCode: pairing.credential,
700 access: "auto",
701 expectedEnvironmentId: box.environmentId,
702 })
703 .pipe(Effect.flip);
704 expect(own.message).toContain("is this machine");
705 yield* probe(a);
706 expect(yield* linkedSessions(impostor)).toEqual([]);
707 expect(yield* Ref.get(impostor.bearers)).toEqual([]);
708 expect((yield* linkedSessions(b)).length).toBe(1);
709
710 // An address alone that answers as the linked box never takes over its link.
711 yield* Ref.set(impostor.descriptor, box);
712 const second = yield* impostor.auth.issuePairingCredential();
713 const taken = yield* a.links
714 .link({ url: impostor.url, pairingCode: second.credential, access: "auto" })
715 .pipe(Effect.flip);
716 expect(taken.message).toContain("already linked");
717 expect(yield* linkedSessions(impostor)).toEqual([]);
718 expect(Option.getOrUndefined(yield* a.links.get(box.environmentId))?.urls).toEqual([b.url]);
719 // The address the link already has still renews it.
720 const renewal = yield* b.auth.issuePairingCredential();
721 expect(
722 yield* a.links.link({ url: b.url, pairingCode: renewal.credential, access: "auto" }),
723 ).toMatchObject({ urls: [b.url] });
724 }),
725 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
726);
727
728it.effect("linking a picked machine that answers nowhere names it and links nothing", () =>
729 Effect.scoped(
730 Effect.gen(function* () {
731 const impostor = yield* serveApart(descriptorOf("environment-impostor", "Impostor"));
732 const a = yield* makeA(laptop);
733 const failed = yield* a.links
734 .link({
735 url: impostor.url,
736 alternateUrls: ["http://192.168.1.20:3773"],
737 pairingCode: "unused",
738 access: "auto",
739 expectedEnvironmentId: box.environmentId,
740 expectedLabel: "Box",
741 })
742 .pipe(Effect.flip);
743 expect(failed.reason).toBe("unreachable");
744 expect(failed.message).toBe(
745 `This environment could not reach Box: ${new URL(impostor.url).origin} answered as another environment, Impostor. http://192.168.1.20:3773 is plain http, which would send the link's credentials unencrypted. Use its https address, or its Tailscale address.`,
746 );
747 expect(yield* a.links.list).toEqual([]);
748 expect(yield* linkedSessions(impostor)).toEqual([]);
749
750 // Every address refused: the message says why.
751 const refused = yield* a.links
752 .link({
753 url: "http://192.168.1.20:3773",
754 pairingCode: "unused",
755 access: "auto",
756 expectedEnvironmentId: box.environmentId,
757 expectedLabel: "Box",
758 })
759 .pipe(Effect.flip);
760 expect(refused.message).toContain("could not reach Box");
761 expect(refused.message).toContain("plain http");
762 }),
763 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
764);
765
766it.live("linking an address with nothing listening says so, unlike one that is not T3 Code", () =>
767 Effect.scoped(
768 Effect.gen(function* () {
769 const closed = yield* Effect.scoped(
770 serveRaw(() => Effect.succeed(HttpServerResponse.empty({ status: 404 }))),
771 );
772 const notT3 = yield* serveRaw(() =>
773 Effect.succeed(
774 HttpServerResponse.text("<h1>Not Found</h1>", { status: 404, contentType: "text/html" }),
775 ),
776 );
777 const a = yield* makeA(laptop);
778 const linking = (url: string) =>
779 a.links.link({ url, pairingCode: "unused", access: "auto" }).pipe(Effect.flip);
780 expect((yield* linking(closed)).message).toBe(`${closed} did not answer (ECONNREFUSED).`);
781 expect((yield* linking(notT3)).message).toBe(
782 `${notT3} answered, but not as a T3 Code environment this version understands.`,
783 );
784 expect(yield* a.links.list).toEqual([]);
785 }),
786 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
787);
788
789/** An https listener whose certificate this machine does not trust. */
790const serveUntrusted = Layer.build(
791 Layer.fresh(
792 NodeHttpServer.layerServer(
793 () =>
794 NodeHttps.createServer({
795 key: NodeFS.readFileSync(new URL("./fixtures/localhost-key.pem", import.meta.url)),
796 cert: NodeFS.readFileSync(new URL("./fixtures/localhost-cert.pem", import.meta.url)),
797 }),
798 { port: 0, host: "127.0.0.1" },
799 ),
800 ),
801).pipe(
802 Effect.map((server) => {
803 const address = Context.get(server, HttpServer.HttpServer).address;
804 if (address._tag === "UnixPathAddress") throw new Error("expected a TCP address");
805 return `https://127.0.0.1:${address.port}`;
806 }),
807);
808
809it.effect("names an untrusted certificate instead of calling the peer something else", () =>
810 Effect.scoped(
811 Effect.gen(function* () {
812 const untrusted = yield* serveUntrusted;
813 const a = yield* makeA(laptop);
814 const failed = yield* a.links
815 .link({ url: untrusted, pairingCode: "unused", access: "auto" })
816 .pipe(Effect.flip);
817 expect(failed.reason).toBe("unreachable");
818 expect(failed.message).toContain("certificate this machine rejected");
819 expect(failed.message).toContain("DEPTH_ZERO_SELF_SIGNED_CERT");
820 expect(yield* a.links.list).toEqual([]);
821
822 // A stored link tries its next address past an untrusted one.
823 const b = yield* serveB(box);
824 const pairing = yield* b.auth.issuePairingCredential();
825 yield* a.links.link({ url: b.url, pairingCode: pairing.credential, access: "auto" });
826 yield* a.sql`
827 UPDATE peer_environment_links
828 SET urls_json = ${JSON.stringify([untrusted, b.url])}
829 WHERE environment_id = ${box.environmentId}
830 `;
831 expect((yield* a.links.resolve(box.environmentId)).url).toBe(b.url);
832 expect((yield* a.links.list).map((link) => link.status)).toEqual(["reachable"]);
833 }),
834 ).pipe(Effect.provide(NodeHttpServer.layerTest)),
835);
836
837it("reads Node's TLS verification code from anywhere in a cause chain", () => {
838 for (const code of [
839 "DEPTH_ZERO_SELF_SIGNED_CERT",
840 "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
841 "CERT_HAS_EXPIRED",
842 "ERR_TLS_CERT_ALTNAME_INVALID",
843 "HOSTNAME_MISMATCH",
844 "INVALID_CA",
845 "INVALID_PURPOSE",
846 "PATH_LENGTH_EXCEEDED",
847 ]) {
848 expect(PeerLinks.certificateErrorCode({ code })).toBe(code);
849 }
850 const wrapped = { message: "fetch failed", cause: { cause: { code: "INVALID_CA" } } };
851 expect(PeerLinks.certificateErrorCode(wrapped)).toBe("INVALID_CA");
852 expect(PeerLinks.certificateErrorCode({ cause: { code: "ECONNREFUSED" } })).toBeUndefined();
853 expect(PeerLinks.certificateErrorCode(new Error("timed out"))).toBeUndefined();
854});