Skip to content
Source
Sign inSign up
Open History to see the last commit

peerLink.ts

131 lines5.0 KB

1import * as Schema from "effect/Schema";
2​
3import { AuthMcpClientAccess } from "./auth.ts";
4import { EnvironmentId, ThreadId, TrimmedNonEmptyString, TurnItemId } from "./baseSchemas.ts";
5​
6/**
7 * A link from this environment to another one (the peer). This environment
8 * signed in to the peer's `/mcp` as an outside agent, with a pairing code from
9 * the peer, so its agents can work there. The peer enforces the access chosen
10 * at linking, and revokes the link from its own Connections.
11 */
12export const PeerLink = Schema.Struct({
13 environmentId: EnvironmentId,
14 label: Schema.String,
15 /** Addresses the peer was linked at, tried in order. */
16 urls: Schema.Array(TrimmedNonEmptyString),
17 access: AuthMcpClientAccess,
18 linkedAt: Schema.DateTimeUtc,
19 expiresAt: Schema.DateTimeUtc,
20 lastReachedAt: Schema.NullOr(Schema.DateTimeUtc),
21 lastError: Schema.NullOr(Schema.String),
22});
23export type PeerLink = typeof PeerLink.Type;
24​
25/** Whether a link can be used now, as worked out when it is listed. */
26export const PeerLinkStatus = Schema.Literals(["reachable", "unreachable", "expired"]);
27export type PeerLinkStatus = typeof PeerLinkStatus.Type;
28​
29export const PeerLinkSummary = Schema.Struct({
30 ...PeerLink.fields,
31 status: PeerLinkStatus,
32});
33export type PeerLinkSummary = typeof PeerLinkSummary.Type;
34​
35export const PeerLinkCreateInput = Schema.Struct({
36 /** Where the peer answers: an https, Tailscale, LAN or loopback base URL. */
37 url: TrimmedNonEmptyString,
38 /** More addresses for the same peer, tried after `url`. */
39 alternateUrls: Schema.optionalKey(Schema.Array(TrimmedNonEmptyString)),
40 /** A pairing code from the peer (`t3 pair` there, or its Connections settings). */
41 pairingCode: TrimmedNonEmptyString,
42 access: AuthMcpClientAccess,
43 /**
44 * The environment the user means. Addresses that answer as another one are
45 * skipped, and only those that answer as this one are kept.
46 */
47 expectedEnvironmentId: Schema.optionalKey(EnvironmentId),
48 /** What the user calls that environment, to name it when it cannot be reached. */
49 expectedLabel: Schema.optionalKey(TrimmedNonEmptyString),
50});
51export type PeerLinkCreateInput = typeof PeerLinkCreateInput.Type;
52​
53export const PeerLinkRemoveInput = Schema.Struct({ environmentId: EnvironmentId });
54export type PeerLinkRemoveInput = typeof PeerLinkRemoveInput.Type;
55​
56export const PeerLinkRemoveResult = Schema.Struct({ removed: Schema.Boolean });
57export type PeerLinkRemoveResult = typeof PeerLinkRemoveResult.Type;
58​
59/** Why linking or calling a peer failed, in words a user can act on. */
60export class PeerLinkError extends Schema.TaggedError<PeerLinkError>()("PeerLinkError", {
61 reason: Schema.Literals([
62 "unreachable",
63 "not_a_t3_environment",
64 "incompatible",
65 "self",
66 "pairing_rejected",
67 "expired",
68 "unknown_link",
69 "storage",
70 "disabled",
71 ]),
72 message: Schema.String,
73 cause: Schema.optionalKey(Schema.Defect()),
74}) {}
75​
76/**
77 * The user's answer to an agent asking to link an environment. The pairing
78 * code is used once to link and is never recorded; the thread learns only the
79 * outcome and the access the user chose.
80 */
81export const PeerLinkRequestAnswerInput = Schema.Struct({
82 threadId: ThreadId,
83 turnItemId: TurnItemId,
84 answer: Schema.Union([
85 Schema.Struct({
86 type: Schema.Literal("link"),
87 /** The machine the user picked, when they picked one rather than typing an address. */
88 environmentId: Schema.optionalKey(EnvironmentId),
89 /** What the client calls that machine. */
90 label: Schema.optionalKey(TrimmedNonEmptyString),
91 /** The addresses the client knows for that machine, most preferred first. */
92 urls: Schema.NonEmptyArray(TrimmedNonEmptyString),
93 access: AuthMcpClientAccess,
94 pairingCode: TrimmedNonEmptyString,
95 }),
96 /** Answers with a machine this environment is already linked to, as it is. */
97 Schema.Struct({ type: Schema.Literal("use-existing"), environmentId: EnvironmentId }),
98 Schema.Struct({ type: Schema.Literal("decline") }),
99 ]),
100});
101export type PeerLinkRequestAnswerInput = typeof PeerLinkRequestAnswerInput.Type;
102​
103const PEER_LINK_REQUEST_FAILURE_MESSAGES = {
104 load_failed: "Could not load the link request.",
105 not_found: "This link request no longer exists.",
106 already_answered: "This link request was already answered.",
107 record_failed: "Could not update the link request.",
108} as const;
109​
110export const PeerLinkRequestFailureReason = Schema.Literals(
111 Object.keys(PEER_LINK_REQUEST_FAILURE_MESSAGES) as Array<
112 keyof typeof PEER_LINK_REQUEST_FAILURE_MESSAGES
113 >,
114);
115export type PeerLinkRequestFailureReason = typeof PeerLinkRequestFailureReason.Type;
116​
117/**
118 * Answering a link request failed before anything was linked. A link the
119 * other environment refused is not this error: the card records it as failed.
120 */
121export class PeerLinkRequestError extends Schema.TaggedError<PeerLinkRequestError>()(
122 "PeerLinkRequestError",
123 {
124 reason: PeerLinkRequestFailureReason,
125 cause: Schema.optionalKey(Schema.Defect()),
126 },
127) {
128 override get message(): string {
129 return PEER_LINK_REQUEST_FAILURE_MESSAGES[this.reason];
130 }