managed-deepagents 0.9.4-dev.9 → 0.10.0-dev.10

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.
Files changed (157) hide show
  1. package/README.md +345 -19
  2. package/dist/agent-auth.d.ts +1 -1
  3. package/dist/agent-auth.d.ts.map +1 -1
  4. package/dist/agent-auth.js +36 -12
  5. package/dist/agent-auth.js.map +1 -1
  6. package/dist/channels/github/connections.d.ts +11 -0
  7. package/dist/channels/github/connections.d.ts.map +1 -0
  8. package/dist/channels/github/connections.js +14 -0
  9. package/dist/channels/github/connections.js.map +1 -0
  10. package/dist/channels/github/index.d.ts +8 -0
  11. package/dist/channels/github/index.d.ts.map +1 -0
  12. package/dist/channels/github/index.js +268 -0
  13. package/dist/channels/github/index.js.map +1 -0
  14. package/dist/channels/github/schema.d.ts +3894 -0
  15. package/dist/channels/github/schema.d.ts.map +1 -0
  16. package/dist/channels/github/schema.js +188 -0
  17. package/dist/channels/github/schema.js.map +1 -0
  18. package/dist/channels/github/types.d.ts +138 -0
  19. package/dist/channels/github/types.d.ts.map +1 -0
  20. package/dist/channels/github/types.js +2 -0
  21. package/dist/channels/github/types.js.map +1 -0
  22. package/dist/channels/http/endpoint.d.ts +41 -0
  23. package/dist/channels/http/endpoint.d.ts.map +1 -0
  24. package/dist/channels/http/endpoint.js +132 -0
  25. package/dist/channels/http/endpoint.js.map +1 -0
  26. package/dist/channels/http/http.d.ts.map +1 -1
  27. package/dist/channels/http/http.js +301 -49
  28. package/dist/channels/http/http.js.map +1 -1
  29. package/dist/channels/http/index.d.ts +2 -1
  30. package/dist/channels/http/index.d.ts.map +1 -1
  31. package/dist/channels/http/index.js +11 -1
  32. package/dist/channels/http/index.js.map +1 -1
  33. package/dist/channels/http/ingress.d.ts +61 -5
  34. package/dist/channels/http/ingress.d.ts.map +1 -1
  35. package/dist/channels/http/ingress.js +138 -29
  36. package/dist/channels/http/ingress.js.map +1 -1
  37. package/dist/channels/http/provider.d.ts +99 -0
  38. package/dist/channels/http/provider.d.ts.map +1 -0
  39. package/dist/channels/http/provider.js +188 -0
  40. package/dist/channels/http/provider.js.map +1 -0
  41. package/dist/channels/http/reply.d.ts +20 -0
  42. package/dist/channels/http/reply.d.ts.map +1 -0
  43. package/dist/channels/http/reply.js +116 -0
  44. package/dist/channels/http/reply.js.map +1 -0
  45. package/dist/channels/http/studio.d.ts +17 -0
  46. package/dist/channels/http/studio.d.ts.map +1 -0
  47. package/dist/channels/http/studio.js +82 -0
  48. package/dist/channels/http/studio.js.map +1 -0
  49. package/dist/channels/http/threads.d.ts +7 -0
  50. package/dist/channels/http/threads.d.ts.map +1 -0
  51. package/dist/channels/http/threads.js +10 -0
  52. package/dist/channels/http/threads.js.map +1 -0
  53. package/dist/channels/http/types.d.ts +113 -1
  54. package/dist/channels/http/types.d.ts.map +1 -1
  55. package/dist/channels/linear/connections.d.ts +11 -0
  56. package/dist/channels/linear/connections.d.ts.map +1 -0
  57. package/dist/channels/linear/connections.js +14 -0
  58. package/dist/channels/linear/connections.js.map +1 -0
  59. package/dist/channels/linear/graphql.d.ts +57 -0
  60. package/dist/channels/linear/graphql.d.ts.map +1 -0
  61. package/dist/channels/linear/graphql.js +167 -0
  62. package/dist/channels/linear/graphql.js.map +1 -0
  63. package/dist/channels/linear/index.d.ts +9 -0
  64. package/dist/channels/linear/index.d.ts.map +1 -0
  65. package/dist/channels/linear/index.js +477 -0
  66. package/dist/channels/linear/index.js.map +1 -0
  67. package/dist/channels/linear/schema.d.ts +1064 -0
  68. package/dist/channels/linear/schema.d.ts.map +1 -0
  69. package/dist/channels/linear/schema.js +249 -0
  70. package/dist/channels/linear/schema.js.map +1 -0
  71. package/dist/channels/linear/types.d.ts +150 -0
  72. package/dist/channels/linear/types.d.ts.map +1 -0
  73. package/dist/channels/linear/types.js +2 -0
  74. package/dist/channels/linear/types.js.map +1 -0
  75. package/dist/channels/message-schema.d.ts +2 -0
  76. package/dist/channels/message-schema.d.ts.map +1 -1
  77. package/dist/channels/message-schema.js +4 -0
  78. package/dist/channels/message-schema.js.map +1 -1
  79. package/dist/channels/reserved-connections.d.ts +12 -0
  80. package/dist/channels/reserved-connections.d.ts.map +1 -0
  81. package/dist/channels/reserved-connections.js +45 -0
  82. package/dist/channels/reserved-connections.js.map +1 -0
  83. package/dist/channels/run-input.d.ts +54 -0
  84. package/dist/channels/run-input.d.ts.map +1 -0
  85. package/dist/channels/run-input.js +129 -0
  86. package/dist/channels/run-input.js.map +1 -0
  87. package/dist/channels/source-providers.js +1 -1
  88. package/dist/channels/source-providers.js.map +1 -1
  89. package/dist/channels/trigger/client.js +2 -6
  90. package/dist/channels/trigger/client.js.map +1 -1
  91. package/dist/channels/trigger/http.js +1 -1
  92. package/dist/channels/trigger/http.js.map +1 -1
  93. package/dist/channels/trigger/identity-context.d.ts +1 -1
  94. package/dist/channels/trigger/identity-context.d.ts.map +1 -1
  95. package/dist/channels/trigger/identity-context.js +2 -3
  96. package/dist/channels/trigger/identity-context.js.map +1 -1
  97. package/dist/channels/trigger/ingress.d.ts.map +1 -1
  98. package/dist/channels/trigger/ingress.js +1 -19
  99. package/dist/channels/trigger/ingress.js.map +1 -1
  100. package/dist/connections.d.ts +31 -0
  101. package/dist/connections.d.ts.map +1 -1
  102. package/dist/connections.js +106 -9
  103. package/dist/connections.js.map +1 -1
  104. package/dist/credential-gate.d.ts +2 -0
  105. package/dist/credential-gate.d.ts.map +1 -1
  106. package/dist/credential-gate.js +37 -3
  107. package/dist/credential-gate.js.map +1 -1
  108. package/dist/identity/loopback.d.ts +1 -1
  109. package/dist/identity/loopback.d.ts.map +1 -1
  110. package/dist/identity/loopback.js +2 -3
  111. package/dist/identity/loopback.js.map +1 -1
  112. package/dist/index.d.ts +6 -1
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +1 -0
  115. package/dist/index.js.map +1 -1
  116. package/dist/ingress/runs.d.ts +50 -1
  117. package/dist/ingress/runs.d.ts.map +1 -1
  118. package/dist/ingress/runs.js +128 -2
  119. package/dist/ingress/runs.js.map +1 -1
  120. package/dist/memory/instructions-middleware.d.ts.map +1 -1
  121. package/dist/memory/instructions-middleware.js +5 -6
  122. package/dist/memory/instructions-middleware.js.map +1 -1
  123. package/dist/namespaces.d.ts +21 -0
  124. package/dist/namespaces.d.ts.map +1 -1
  125. package/dist/namespaces.js +4 -0
  126. package/dist/namespaces.js.map +1 -1
  127. package/dist/runtime/auth.d.ts +2 -1
  128. package/dist/runtime/auth.d.ts.map +1 -1
  129. package/dist/runtime/auth.js +115 -14
  130. package/dist/runtime/auth.js.map +1 -1
  131. package/dist/runtime/channel-icon.d.ts +22 -0
  132. package/dist/runtime/channel-icon.d.ts.map +1 -0
  133. package/dist/runtime/channel-icon.js +70 -0
  134. package/dist/runtime/channel-icon.js.map +1 -0
  135. package/dist/runtime/channel.d.ts +6 -0
  136. package/dist/runtime/channel.d.ts.map +1 -1
  137. package/dist/runtime/channel.js.map +1 -1
  138. package/dist/runtime/identity-http.d.ts +2 -0
  139. package/dist/runtime/identity-http.d.ts.map +1 -1
  140. package/dist/runtime/identity-http.js +36 -11
  141. package/dist/runtime/identity-http.js.map +1 -1
  142. package/dist/runtime/identity-runtime.d.ts +15 -0
  143. package/dist/runtime/identity-runtime.d.ts.map +1 -1
  144. package/dist/runtime/identity-runtime.js +35 -0
  145. package/dist/runtime/identity-runtime.js.map +1 -1
  146. package/dist/runtime/index.d.ts.map +1 -1
  147. package/dist/runtime/index.js +2 -0
  148. package/dist/runtime/index.js.map +1 -1
  149. package/dist/runtime/memory/mounts.d.ts +0 -42
  150. package/dist/runtime/memory/mounts.d.ts.map +1 -1
  151. package/dist/runtime/memory/mounts.js +7 -63
  152. package/dist/runtime/memory/mounts.js.map +1 -1
  153. package/dist/runtime/sandbox-manager.d.ts +1 -1
  154. package/dist/runtime/sandbox-manager.d.ts.map +1 -1
  155. package/dist/runtime/sandbox-manager.js +3 -3
  156. package/dist/runtime/sandbox-manager.js.map +1 -1
  157. package/package.json +8 -8
@@ -0,0 +1,167 @@
1
+ import { z } from "zod";
2
+ import { connections } from "../../connections.js";
3
+ import { CachedRead } from "../http/provider.js";
4
+ import { nonblankStringSchema } from "../message-schema.js";
5
+ import { LINEAR_TOKEN_SLUG } from "./connections.js";
6
+ const LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
7
+ const LINEAR_API_TIMEOUT_MS = 10_000;
8
+ /** The largest Linear response body read. */
9
+ const LINEAR_RESPONSE_MAX_CHARS = 1_000_000;
10
+ /** How long the app token read from Agent Auth is reused. */
11
+ const TOKEN_TTL_MS = 5 * 60_000;
12
+ /**
13
+ * How long the app's user is reused. It changes only when the app is
14
+ * installed anew, which a long-lived process should notice too.
15
+ */
16
+ const APP_USER_TTL_MS = 60 * 60_000;
17
+ /**
18
+ * Where GraphQL requests go: Linear's API, or under `mda dev`
19
+ * (`MDA_LOCAL_DEV=1`) the stand-in `MDA_LINEAR_API_URL` names, so local
20
+ * tests never reach Linear. A deployment always uses Linear's API.
21
+ */
22
+ function graphqlUrl(env = process.env) {
23
+ const override = env.MDA_LINEAR_API_URL?.trim();
24
+ if (env.MDA_LOCAL_DEV !== "1" || !override)
25
+ return LINEAR_GRAPHQL_URL;
26
+ const url = new URL(override);
27
+ if (url.protocol !== "http:" && url.protocol !== "https:")
28
+ throw new TypeError("MDA_LINEAR_API_URL must be an http(s) URL");
29
+ return url.toString();
30
+ }
31
+ /** Linear refused a request for exceeding its rate limit. */
32
+ export class LinearRateLimitError extends Error {
33
+ constructor() {
34
+ super("Linear rate limit exceeded");
35
+ this.name = "LinearRateLimitError";
36
+ }
37
+ }
38
+ /** The app's access token, read from Agent Auth at most every few minutes. */
39
+ const appToken = new CachedRead(() => connections.resolveDeploymentSecret(LINEAR_TOKEN_SLUG), () => TOKEN_TTL_MS);
40
+ /** Forget the cached app token; for tests. */
41
+ export function resetLinearToken() {
42
+ appToken.reset();
43
+ }
44
+ function errorCodes(body) {
45
+ return (body.errors ?? []).flatMap((error) => typeof error.extensions?.code === "string" ? [error.extensions.code] : []);
46
+ }
47
+ /**
48
+ * Run a GraphQL operation as the app. `query` is a constant; everything that
49
+ * varies goes in `variables`. A rejected token is read again once. Returns
50
+ * the response's `data`, checked against `schema`.
51
+ */
52
+ export async function linearRequest(query, variables, schema) {
53
+ const send = async () => fetch(graphqlUrl(), {
54
+ method: "POST",
55
+ headers: {
56
+ Authorization: `Bearer ${await appToken.get()}`,
57
+ "Content-Type": "application/json",
58
+ },
59
+ body: JSON.stringify({ query, variables }),
60
+ signal: AbortSignal.timeout(LINEAR_API_TIMEOUT_MS),
61
+ });
62
+ let response = await send();
63
+ let body = await readBody(response);
64
+ if (response.status === 401 ||
65
+ errorCodes(body).includes("AUTHENTICATION_ERROR")) {
66
+ appToken.reset();
67
+ response = await send();
68
+ body = await readBody(response);
69
+ }
70
+ if (errorCodes(body).includes("RATELIMITED"))
71
+ throw new LinearRateLimitError();
72
+ if (!response.ok || body.errors?.length || body.data === undefined)
73
+ throw new Error(`Linear refused the request (HTTP ${response.status})`);
74
+ const data = schema.safeParse(body.data);
75
+ if (!data.success)
76
+ throw new Error("Linear answered in an unexpected shape");
77
+ return data.data;
78
+ }
79
+ async function readBody(response) {
80
+ const text = await response.text();
81
+ if (text.length > LINEAR_RESPONSE_MAX_CHARS)
82
+ return {};
83
+ try {
84
+ const parsed = JSON.parse(text);
85
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
86
+ ? parsed
87
+ : {};
88
+ }
89
+ catch {
90
+ return {};
91
+ }
92
+ }
93
+ const VIEWER_QUERY = "query MdaLinearViewer { viewer { id displayName } }";
94
+ const viewerSchema = z.object({
95
+ viewer: z.object({ id: nonblankStringSchema, displayName: z.string() }),
96
+ });
97
+ /**
98
+ * The Linear user the app acts as, read at most hourly: its own comments and
99
+ * changes are ignored, and a comment mentioning its `displayName` starts an
100
+ * agent session.
101
+ */
102
+ const appUser = new CachedRead(async () => (await linearRequest(VIEWER_QUERY, {}, viewerSchema)).viewer, () => APP_USER_TTL_MS);
103
+ export function linearAppUser() {
104
+ return appUser.get();
105
+ }
106
+ /** Forget the cached app user; for tests. */
107
+ export function resetLinearAppUser() {
108
+ appUser.reset();
109
+ }
110
+ const COMMENT_CREATE_MUTATION = `mutation MdaLinearCommentCreate($input: CommentCreateInput!) {
111
+ commentCreate(input: $input) { success comment { id url } }
112
+ }`;
113
+ const commentCreateSchema = z.object({
114
+ commentCreate: z.object({
115
+ success: z.literal(true),
116
+ comment: z.object({ id: nonblankStringSchema, url: z.string().nullish() }),
117
+ }),
118
+ });
119
+ /**
120
+ * Comment on an issue as the app, threaded under `parentId` when given.
121
+ * Returns the comment's id and URL.
122
+ */
123
+ export async function createLinearComment(input) {
124
+ const { comment } = (await linearRequest(COMMENT_CREATE_MUTATION, { input }, commentCreateSchema)).commentCreate;
125
+ return { id: comment.id, ...(comment.url && { url: comment.url }) };
126
+ }
127
+ const COMMENT_SESSION_QUERY = `query MdaLinearCommentSession($id: String!) {
128
+ comment(id: $id) { agentSession { id } }
129
+ }`;
130
+ const commentSessionSchema = z.object({
131
+ comment: z
132
+ .object({ agentSession: z.object({ id: nonblankStringSchema }).nullable() })
133
+ .nullable(),
134
+ });
135
+ /** Whether a comment belongs to an agent session's thread. */
136
+ export async function commentInAgentSession(commentId) {
137
+ const { comment } = await linearRequest(COMMENT_SESSION_QUERY, { id: commentId }, commentSessionSchema);
138
+ return Boolean(comment?.agentSession);
139
+ }
140
+ const ACTIVITY_CREATE_MUTATION = `mutation MdaLinearActivityCreate($input: AgentActivityCreateInput!) {
141
+ agentActivityCreate(input: $input) { success agentActivity { id } }
142
+ }`;
143
+ const activityCreateSchema = z.object({
144
+ agentActivityCreate: z.object({
145
+ success: z.literal(true),
146
+ agentActivity: z.object({ id: nonblankStringSchema }),
147
+ }),
148
+ });
149
+ /**
150
+ * Post an activity to an agent session as the app. An `ephemeral` thought is
151
+ * replaced by the next activity.
152
+ */
153
+ export async function createLinearActivity(input) {
154
+ const { agentActivity } = (await linearRequest(ACTIVITY_CREATE_MUTATION, { input }, activityCreateSchema)).agentActivityCreate;
155
+ return { id: agentActivity.id };
156
+ }
157
+ const SESSION_UPDATE_MUTATION = `mutation MdaLinearSessionUpdate($id: String!, $input: AgentSessionUpdateInput!) {
158
+ agentSessionUpdate(id: $id, input: $input) { success }
159
+ }`;
160
+ const sessionUpdateSchema = z.object({
161
+ agentSessionUpdate: z.object({ success: z.literal(true) }),
162
+ });
163
+ /** Link an agent session to a page about it, such as its Studio thread. */
164
+ export async function addLinearSessionUrl(agentSessionId, link) {
165
+ await linearRequest(SESSION_UPDATE_MUTATION, { id: agentSessionId, input: { addedExternalUrls: [link] } }, sessionUpdateSchema);
166
+ }
167
+ //# sourceMappingURL=graphql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql.js","sourceRoot":"","sources":["../../../src/channels/linear/graphql.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import { type HttpChannel } from "../http/index.js";
2
+ import type { LinearChannelOptions } from "./types.js";
3
+ export type { LinearAppOptions, LinearChannelOptions, LinearChannelThread, LinearEvent, LinearEventCallback, LinearEventKey, LinearEventResult, LinearPayloadFor, } from "./types.js";
4
+ export { LinearRateLimitError } from "./graphql.js";
5
+ /** Adapt verified Linear webhooks into HTTP channel messages or skips. */
6
+ export declare function linearChannel(options?: LinearChannelOptions): HttpChannel;
7
+ /** Forget the cached webhook secret; for tests. */
8
+ export declare function resetLinearWebhookSecret(): void;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/channels/linear/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,WAAW,EAOjB,MAAM,kBAAkB,CAAC;AAkC1B,OAAO,KAAK,EAEV,oBAAoB,EAKrB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AA2BpD,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,WAAW,CA+D7E;AA+CD,mDAAmD;AACnD,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C"}
@@ -0,0 +1,477 @@
1
+ import { z } from "zod";
2
+ import { channelThreadId, httpChannel, } from "../http/index.js";
3
+ import { channelName, checkEventKeys, definedFields, eventSnapshot, handledEventKeys, readDeploymentSecret, resetDeploymentSecrets, resolveEndpoints, runEventHandler, settleEvents, verifyHmacSha256, } from "../http/provider.js";
4
+ import { interruptedNotice, studioUrl } from "../http/studio.js";
5
+ import { requireTextContent } from "../runtime.js";
6
+ import { logger } from "../../runtime/log.js";
7
+ import { validateChannelIconPath } from "../../runtime/channel-icon.js";
8
+ import { linearAppConnectionSlug } from "./connections.js";
9
+ import { addLinearSessionUrl, commentInAgentSession, createLinearActivity, createLinearComment, linearAppUser, } from "./graphql.js";
10
+ import { SUPPORTED_EVENT_KEYS, linearEventSchema, linearReceiptSchema, } from "./schema.js";
11
+ export { LinearRateLimitError } from "./graphql.js";
12
+ const EVENT_KEYS = new Set(SUPPORTED_EVENT_KEYS);
13
+ const SESSION_KEYS = new Set([
14
+ "AgentSessionEvent.created",
15
+ "AgentSessionEvent.prompted",
16
+ ]);
17
+ const APP_OPTION_KEYS = new Set([
18
+ "name",
19
+ "description",
20
+ "icon",
21
+ "teams",
22
+ ]);
23
+ /** Whether a delivery's `type.action` is an event key the channel supports. */
24
+ function isLinearEventKey(key) {
25
+ return EVENT_KEYS.has(key);
26
+ }
27
+ /** The part of a delivery's body the replay check reads. */
28
+ const timestampSchema = z.looseObject({ webhookTimestamp: z.number() });
29
+ /** Adapt verified Linear webhooks into HTTP channel messages or skips. */
30
+ export function linearChannel(options = {}) {
31
+ const handlers = resolveEndpoints("Linear", options.onEvent);
32
+ checkEventKeys("Linear", handlers, EVENT_KEYS);
33
+ // A misspelled option would silently widen what starts runs.
34
+ const unknown = Object.keys(options.app ?? {}).filter((key) => !APP_OPTION_KEYS.has(key));
35
+ if (unknown.length)
36
+ throw new TypeError(`unknown Linear app option ${unknown.join(", ")}; use ${[...APP_OPTION_KEYS].join(", ")}`);
37
+ if (options.app)
38
+ validateAppDisplay(options.app);
39
+ const teams = options.app?.teams?.map((team) => team.trim().toLowerCase());
40
+ if (teams?.length === 0 || teams?.some((team) => !team))
41
+ throw new TypeError("Linear app.teams must list team keys; omit it to allow every team");
42
+ const filters = {
43
+ teams,
44
+ ignoreBots: options.ignoreBots ?? true,
45
+ sessions: options.sessions ?? true,
46
+ };
47
+ const channel = httpChannel({
48
+ provider: "linear",
49
+ ...(options.multitaskStrategy && {
50
+ multitaskStrategy: options.multitaskStrategy,
51
+ }),
52
+ providerConfig: {
53
+ // Deploy subscribes the app's webhook to the handled events.
54
+ events: handledEvents(handlers, filters.sessions),
55
+ ...(options.app && {
56
+ app: definedFields({
57
+ name: options.app.name?.trim(),
58
+ description: options.app.description?.trim(),
59
+ icon: options.app.icon &&
60
+ validateChannelIconPath(options.app.icon, "Linear app.icon"),
61
+ teams: options.app.teams && [...options.app.teams],
62
+ }),
63
+ }),
64
+ },
65
+ verify: verifyLinearRequest,
66
+ parse: (context) => parseLinearRequest(context, handlers, channelName(channel), filters),
67
+ input: (message, rawEvent) => handlerInput(handlers, rawEvent, {
68
+ id: message.threadId,
69
+ channel: { name: channelName(channel) },
70
+ }),
71
+ post: postReply,
72
+ onReceived: acknowledgeSession,
73
+ onError: reportError,
74
+ onSettled: settleEvents(notifySettled, options.onSettled, linearEvent, () => channelName(channel)),
75
+ });
76
+ // Served at /channels/{name}/icon; deploy points the app's icon at it.
77
+ const icon = options.app?.icon;
78
+ return icon === undefined
79
+ ? channel
80
+ : Object.assign(channel, {
81
+ icon: validateChannelIconPath(icon, "Linear app.icon"),
82
+ });
83
+ }
84
+ /** The events deploy subscribes the app's webhook to. */
85
+ function handledEvents(handlers, sessions) {
86
+ return [
87
+ ...new Set([
88
+ ...handledEventKeys(handlers),
89
+ ...(sessions ? SESSION_KEYS : []),
90
+ ]),
91
+ ].sort();
92
+ }
93
+ /** Check how the app looks against Linear's rules, before deploy sends it. */
94
+ function validateAppDisplay({ name, description, icon }) {
95
+ if (name !== undefined) {
96
+ const trimmed = name.trim();
97
+ if (trimmed.length < 2 ||
98
+ trimmed.length > 80 ||
99
+ /linear|https?:\/\//i.test(trimmed))
100
+ throw new TypeError('Linear app.name must be 2-80 characters, without "Linear" or a URL');
101
+ }
102
+ if (description !== undefined &&
103
+ (!description.trim() || description.trim().length > 1000))
104
+ throw new TypeError("Linear app.description must be 1-1000 characters");
105
+ if (icon !== undefined)
106
+ validateChannelIconPath(icon, "Linear app.icon");
107
+ }
108
+ /**
109
+ * The app's webhook signing secret: `LINEAR_WEBHOOK_SECRET` when set,
110
+ * otherwise the agent secret stored at
111
+ * `mda/linear-app/{deployment}/webhook-secret`. Only a missing connection
112
+ * reads as `undefined`; credential store failures propagate.
113
+ */
114
+ function linearWebhookSecret() {
115
+ return readDeploymentSecret("LINEAR_WEBHOOK_SECRET", (deploymentId) => linearAppConnectionSlug(deploymentId, "webhook-secret"), "Linear webhook secret");
116
+ }
117
+ /** Forget the cached webhook secret; for tests. */
118
+ export function resetLinearWebhookSecret() {
119
+ resetDeploymentSecrets();
120
+ }
121
+ /**
122
+ * How far a delivery's `webhookTimestamp` may be from now. Linear recommends
123
+ * rejecting older payloads, so a captured delivery can't be replayed.
124
+ */
125
+ const WEBHOOK_TOLERANCE_MS = 60_000;
126
+ async function verifyLinearRequest({ request, rawBody, }) {
127
+ // Reject unsigned deliveries before reading the secret from Agent Auth.
128
+ const signature = request.headers.get("linear-signature") ?? "";
129
+ if (!/^[0-9a-f]{64}$/i.test(signature))
130
+ return false;
131
+ const secret = await linearWebhookSecret();
132
+ if (!secret)
133
+ return false;
134
+ if (!verifyHmacSha256(secret, rawBody, signature))
135
+ return false;
136
+ return isFresh(rawBody);
137
+ }
138
+ /** Whether a signed delivery was sent within the replay window. */
139
+ function isFresh(rawBody) {
140
+ let body;
141
+ try {
142
+ body = JSON.parse(new TextDecoder().decode(rawBody));
143
+ }
144
+ catch {
145
+ // Signed, but not JSON: no timestamp to trust, so the delivery is refused.
146
+ return false;
147
+ }
148
+ const parsed = timestampSchema.safeParse(body);
149
+ return (parsed.success &&
150
+ Math.abs(Date.now() - parsed.data.webhookTimestamp) <= WEBHOOK_TOLERANCE_MS);
151
+ }
152
+ /**
153
+ * Whether another app or an integration made the change. The delivery's actor
154
+ * says who acted. An issue's or comment's `botActor` is the bot that created
155
+ * it, so it names the actor only when the event is its creation.
156
+ */
157
+ function byBot(event) {
158
+ if (event.actor?.type !== undefined &&
159
+ event.actor.type.toLowerCase() !== "user")
160
+ return true;
161
+ if (event.key === "Issue.update")
162
+ return false;
163
+ const { botActor } = event.payload.data;
164
+ return botActor !== undefined && botActor !== null;
165
+ }
166
+ async function parseLinearRequest({ request }, handlers, channelName, filters) {
167
+ const receipt = linearReceiptSchema.parse({
168
+ deliveryId: request.headers.get("linear-delivery"),
169
+ payload: await request.json(),
170
+ });
171
+ const { deliveryId, payload } = receipt;
172
+ const key = `${payload.type}.${payload.action}`;
173
+ logger.info(`linear receipt ${JSON.stringify({
174
+ event: logger.logIdentifier(key),
175
+ deliveryId: logger.logIdentifier(deliveryId),
176
+ })}`);
177
+ // Linear expects a 200 for every delivery it should not retry.
178
+ const response = Response.json({ status: "received" }, { status: 200 });
179
+ if (key === "OAuthApp.revoked") {
180
+ logger.warn("Linear app access was revoked; reinstall it with mda deploy");
181
+ return { type: "ignore", response };
182
+ }
183
+ // Events nothing handles stop here, before run creation. Sessions have a
184
+ // default handler while they are on.
185
+ const handled = isLinearEventKey(key) &&
186
+ ((filters.sessions && SESSION_KEYS.has(key)) ||
187
+ (Object.hasOwn(handlers, key) && handlers[key] !== undefined));
188
+ if (!handled)
189
+ return { type: "ignore", response };
190
+ const event = linearEventSchema.parse({ key, deliveryId, payload });
191
+ // Issues of teams the channel is not limited to start nothing.
192
+ if (filters.teams &&
193
+ !(event.team && filters.teams.includes(event.team.key.toLowerCase()))) {
194
+ logger.info(`linear event ignored: ${event.team ? "its team is not in app.teams" : "it names no team"} ${JSON.stringify({
195
+ event: logger.logIdentifier(key),
196
+ deliveryId: logger.logIdentifier(deliveryId),
197
+ })}`);
198
+ return { type: "ignore", response };
199
+ }
200
+ if (!("sessionId" in event) && (await ignored(event, filters)))
201
+ return { type: "ignore", response };
202
+ // Preserve the original event even if a callback modifies its argument.
203
+ const snapshot = eventSnapshot(event, "Linear");
204
+ // One thread per agent session, so a Stop ends only its own session's
205
+ // work. Other events share one thread per issue, so the agent keeps the
206
+ // conversation.
207
+ const conversation = `${event.organizationId}/${event.issueId}`;
208
+ const message = {
209
+ threadId: channelThreadId(channelName, "sessionId" in event
210
+ ? `${conversation}/session/${event.sessionId}`
211
+ : conversation),
212
+ userId: "mda:channel",
213
+ target: linearTarget(event),
214
+ deliveryKey: deliveryId,
215
+ };
216
+ // Stop in a session cancels the session's run.
217
+ if (event.key === "AgentSessionEvent.prompted" &&
218
+ event.payload.agentActivity.signal === "stop")
219
+ return {
220
+ type: "cancel",
221
+ message: {
222
+ userId: message.userId,
223
+ threadId: message.threadId,
224
+ target: message.target,
225
+ },
226
+ rawEvent: snapshot,
227
+ response,
228
+ };
229
+ // What the run starts with, or how the event answers an interrupt, is
230
+ // decided once MDA knows whether the thread waits on one.
231
+ return { type: "message", message, rawEvent: snapshot, response };
232
+ }
233
+ /**
234
+ * Whether a webhook event starts nothing: another app's change, the app's own
235
+ * activity, or a comment an agent session already delivers.
236
+ */
237
+ async function ignored(event, filters) {
238
+ if (filters.ignoreBots && byBot(event))
239
+ return true;
240
+ // Linear reports the app's comments as its user's, not as a bot's.
241
+ const appUser = await linearAppUser();
242
+ if (event.actor?.id === appUser.id)
243
+ return true;
244
+ if (event.key !== "Comment.create")
245
+ return false;
246
+ const comment = event.payload.data;
247
+ if ((comment.user?.id ?? comment.userId) === appUser.id)
248
+ return true;
249
+ if (!filters.sessions)
250
+ return false;
251
+ const delivered = await sessionComment(comment, appUser);
252
+ if (delivered)
253
+ logger.info(`linear comment left to its agent session: ${delivered} ${JSON.stringify({
254
+ event: logger.logIdentifier(event.key),
255
+ deliveryId: logger.logIdentifier(event.deliveryId),
256
+ })}`);
257
+ return delivered !== undefined;
258
+ }
259
+ /**
260
+ * Why a comment is part of an agent session, which delivers it as a session
261
+ * event too: one that mentions the app opens a session, and a reply in a
262
+ * session's thread prompts it. Only a reply without a mention asks Linear.
263
+ */
264
+ async function sessionComment(comment, appUser) {
265
+ if (mentionsApp(comment.body, appUser.displayName))
266
+ return "it mentions the app, which opens a session";
267
+ if (comment.parentId && (await commentInAgentSession(comment.parentId)))
268
+ return "it replies in a session's thread";
269
+ return undefined;
270
+ }
271
+ function mentionsApp(body, displayName) {
272
+ if (!displayName)
273
+ return false;
274
+ const name = displayName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
275
+ return new RegExp(`(^|[^\\w@])@${name}(?![\\w-])`, "i").test(body);
276
+ }
277
+ /** Where replies go: the issue, and the comment thread or session that asked. */
278
+ function linearTarget(event) {
279
+ const target = {
280
+ organizationId: event.organizationId,
281
+ issueId: event.issueId,
282
+ };
283
+ if ("sessionId" in event) {
284
+ const identifier = event.payload.agentSession.issue?.identifier;
285
+ return {
286
+ ...target,
287
+ ...(identifier && { identifier }),
288
+ sessionId: event.sessionId,
289
+ };
290
+ }
291
+ if (event.key === "Comment.create") {
292
+ const { data } = event.payload;
293
+ return {
294
+ ...target,
295
+ ...(data.issue?.identifier && { identifier: data.issue.identifier }),
296
+ // Replies go into the comment's thread; Linear threads one level deep.
297
+ commentId: data.parentId ?? data.id,
298
+ };
299
+ }
300
+ return { ...target, identifier: event.payload.data.identifier };
301
+ }
302
+ const ERROR_MESSAGE = "Something went wrong while working on this.";
303
+ /** What the event's handler starts a run with; `undefined` when it skips or fails. */
304
+ async function handlerInput(handlers, rawEvent, thread) {
305
+ const event = linearEvent(rawEvent);
306
+ if (!event)
307
+ return undefined;
308
+ const outcome = Object.hasOwn(handlers, event.key) && handlers[event.key] !== undefined
309
+ ? await runEventHandler("Linear", event, () => callHandler(handlers, event.key, event, thread))
310
+ : sessionInput(event);
311
+ if (outcome.type === "input")
312
+ return outcome.input;
313
+ // A session that gets no run would wait for an answer that never comes,
314
+ // whether it just opened or this is a follow-up in it. A handler that threw
315
+ // failed; one that returned nothing declined.
316
+ if ("sessionId" in event)
317
+ await say(event.sessionId, outcome.type === "failed"
318
+ ? { type: "error", body: ERROR_MESSAGE }
319
+ : { type: "response", body: "I'm not set up to work on this request." });
320
+ return undefined;
321
+ }
322
+ /** What a session event starts a run with when no handler decides. */
323
+ function sessionInput(event) {
324
+ let input;
325
+ if (event.key === "AgentSessionEvent.created") {
326
+ const { promptContext, agentSession } = event.payload;
327
+ input =
328
+ promptContext?.trim() ||
329
+ agentSession.comment?.body?.trim() ||
330
+ agentSession.issue?.title?.trim() ||
331
+ undefined;
332
+ }
333
+ else if (event.key === "AgentSessionEvent.prompted")
334
+ input = event.payload.agentActivity.content.body?.trim() || undefined;
335
+ return input ? { type: "input", input } : { type: "skipped" };
336
+ }
337
+ /** Call the handler of the event's key, typed by the key. */
338
+ function callHandler(handlers, key, event, thread) {
339
+ return handlers[key]?.(event, thread);
340
+ }
341
+ /**
342
+ * The parsed event a delivery carries, checked against the schema again: the
343
+ * HTTP layer hands over a copy, which handlers may change.
344
+ */
345
+ function linearEvent(rawEvent) {
346
+ // The schema reads `key`, `deliveryId` and `payload`, and derives the rest.
347
+ const parsed = linearEventSchema.safeParse(rawEvent);
348
+ return parsed.success ? parsed.data : undefined;
349
+ }
350
+ /** Say something in a session; a failure is logged, never thrown. */
351
+ async function say(sessionId, content, extra = {}) {
352
+ try {
353
+ await createLinearActivity({
354
+ agentSessionId: sessionId,
355
+ content,
356
+ ...extra,
357
+ });
358
+ }
359
+ catch (error) {
360
+ logger.error(`linear session activity failed (${logger.safeErrorLabel(error)})`);
361
+ }
362
+ }
363
+ /**
364
+ * Post the agent's reply: as the session's response when a session asked,
365
+ * otherwise as a comment in the thread of the comment that started the run.
366
+ * A post that starts a new thread, such as a scheduled one, comments on the
367
+ * issue itself.
368
+ */
369
+ async function postReply(input) {
370
+ const { target, newThread, ...message } = input;
371
+ if (!isLinearTarget(target))
372
+ throw new TypeError("invalid Linear target");
373
+ const body = requireTextContent(message);
374
+ if (target.sessionId && !newThread)
375
+ return createLinearActivity({
376
+ agentSessionId: target.sessionId,
377
+ content: { type: "response", body },
378
+ });
379
+ return createLinearComment({
380
+ issueId: target.issueId,
381
+ body,
382
+ ...(target.commentId && !newThread && { parentId: target.commentId }),
383
+ });
384
+ }
385
+ /**
386
+ * Linear marks a session unresponsive without an activity within seconds, even
387
+ * while the run waits behind another one: acknowledge it at once, and link a
388
+ * new session to its thread in Studio.
389
+ */
390
+ async function acknowledgeSession(target, delivery) {
391
+ if (!isLinearTarget(target) || !target.sessionId)
392
+ return;
393
+ await say(target.sessionId, { type: "thought", body: "Working on it." }, {
394
+ ephemeral: true,
395
+ });
396
+ const url = studioUrl(delivery);
397
+ if (!url ||
398
+ linearEvent(delivery.rawEvent)?.key !== "AgentSessionEvent.created")
399
+ return;
400
+ try {
401
+ await addLinearSessionUrl(target.sessionId, {
402
+ label: "LangSmith Studio",
403
+ url,
404
+ });
405
+ }
406
+ catch (error) {
407
+ logger.error(`linear session link failed (${logger.safeErrorLabel(error)})`);
408
+ }
409
+ }
410
+ /**
411
+ * What the conversation hears once a delivery settles, besides the run's own
412
+ * answer and `reportError`: where to make a pending decision, or, after
413
+ * Stop, the final word Linear waits for.
414
+ */
415
+ async function notifySettled(target, delivery) {
416
+ const { type } = delivery.outcome;
417
+ if (type === "interrupted" || type === "waiting")
418
+ await notifyInterrupted(target, delivery, type);
419
+ else if (type === "stopped")
420
+ await confirmStopped(target);
421
+ }
422
+ /** After Stop, Linear waits for a final word from the agent. */
423
+ async function confirmStopped(target) {
424
+ if (!isLinearTarget(target) || !target.sessionId)
425
+ return;
426
+ await say(target.sessionId, { type: "response", body: "Stopped." });
427
+ }
428
+ /** Tell the session or the issue that the run failed. */
429
+ async function reportError(_error, target) {
430
+ if (!isLinearTarget(target))
431
+ return;
432
+ if (target.sessionId) {
433
+ await createLinearActivity({
434
+ agentSessionId: target.sessionId,
435
+ content: { type: "error", body: ERROR_MESSAGE },
436
+ });
437
+ return;
438
+ }
439
+ await createLinearComment({
440
+ issueId: target.issueId,
441
+ body: ERROR_MESSAGE,
442
+ ...(target.commentId && { parentId: target.commentId }),
443
+ });
444
+ }
445
+ /**
446
+ * The conversation waits on a decision, made in LangSmith Studio: say so with
447
+ * a link. In a session it asks for input, so the session waits rather than
448
+ * ends: once the decision is made, the resumed run's answer is the session's
449
+ * response. Otherwise it is a comment.
450
+ */
451
+ async function notifyInterrupted(target, delivery, outcome) {
452
+ if (!isLinearTarget(target))
453
+ return;
454
+ const body = interruptedNotice(outcome, delivery, "ask again");
455
+ if (target.sessionId) {
456
+ await createLinearActivity({
457
+ agentSessionId: target.sessionId,
458
+ content: { type: "elicitation", body },
459
+ });
460
+ return;
461
+ }
462
+ await createLinearComment({
463
+ issueId: target.issueId,
464
+ body,
465
+ ...(target.commentId && { parentId: target.commentId }),
466
+ });
467
+ }
468
+ function isLinearTarget(value) {
469
+ if (!value || typeof value !== "object" || Array.isArray(value))
470
+ return false;
471
+ const { organizationId, issueId, identifier, commentId, sessionId } = value;
472
+ const nonblank = (field) => typeof field === "string" && field.trim().length > 0;
473
+ return (nonblank(organizationId) &&
474
+ nonblank(issueId) &&
475
+ [identifier, commentId, sessionId].every((field) => field === undefined || nonblank(field)));
476
+ }
477
+ //# sourceMappingURL=index.js.map