cyrus-edge-worker 0.2.15 → 0.2.17

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.
@@ -34,6 +34,8 @@ export declare class EdgeWorker extends EventEmitter {
34
34
  private activeWebhookCount;
35
35
  /** Handler for AskUserQuestion tool invocations via Linear select signal */
36
36
  private askUserQuestionHandler;
37
+ /** User access control for whitelisting/blacklisting Linear users */
38
+ private userAccessControl;
37
39
  constructor(config: EdgeWorkerConfig);
38
40
  /**
39
41
  * Start the edge worker
@@ -133,6 +135,33 @@ export declare class EdgeWorker extends EventEmitter {
133
135
  * Handle issue unassignment webhook
134
136
  */
135
137
  private handleIssueUnassignedWebhook;
138
+ /**
139
+ * Handle issue content update webhook (title, description, or attachments).
140
+ *
141
+ * When the title, description, or attachments of an issue are updated, this handler feeds
142
+ * the changes into any active session for that issue, allowing the AI to
143
+ * compare old vs new values and decide whether to take action.
144
+ *
145
+ * The prompt uses XML-style formatting to clearly show what changed:
146
+ * - <issue_update> wrapper with timestamp and issue identifier
147
+ * - <title_change> with <old_title> and <new_title> if title changed
148
+ * - <description_change> with <old_description> and <new_description> if description changed
149
+ * - <attachments_change> with <old_attachments> and <new_attachments> if attachments changed
150
+ * - <guidance> section instructing the agent to evaluate whether changes affect its work
151
+ *
152
+ * @see https://studio.apollographql.com/public/Linear-Webhooks/variant/current/schema/reference/objects/EntityWebhookPayload
153
+ * @see https://studio.apollographql.com/public/Linear-Webhooks/variant/current/schema/reference/objects/IssueWebhookPayload
154
+ * @see https://studio.apollographql.com/public/Linear-Webhooks/variant/current/schema/reference/unions/DataWebhookPayload
155
+ */
156
+ private handleIssueContentUpdate;
157
+ /**
158
+ * Build an XML-formatted prompt for issue content updates (title, description, attachments).
159
+ *
160
+ * The prompt clearly shows what fields changed by comparing old vs new values,
161
+ * and includes guidance for the agent to evaluate whether these changes affect
162
+ * its current implementation or action plan.
163
+ */
164
+ private buildIssueUpdatePrompt;
136
165
  /**
137
166
  * Get issue tracker for a workspace by finding first repository with that workspace ID
138
167
  */
@@ -509,6 +538,21 @@ export declare class EdgeWorker extends EventEmitter {
509
538
  * Get Agent Sessions for an issue
510
539
  */
511
540
  getAgentSessionsForIssue(issueId: string, repositoryId: string): any[];
541
+ /**
542
+ * Check if the user who triggered the webhook is allowed to interact.
543
+ * @param webhook The webhook containing user information
544
+ * @param repository The repository configuration
545
+ * @returns Access check result with allowed status and user name
546
+ */
547
+ private checkUserAccess;
548
+ /**
549
+ * Handle blocked user according to configured behavior.
550
+ * Posts a response activity to end the session.
551
+ * @param webhook The webhook that triggered the blocked access
552
+ * @param repository The repository configuration
553
+ * @param _reason The reason for blocking (for logging)
554
+ */
555
+ private handleBlockedUser;
512
556
  /**
513
557
  * Load persisted EdgeWorker state for all repositories
514
558
  */
@@ -1 +1 @@
1
- {"version":3,"file":"EdgeWorker.d.ts","sourceRoot":"","sources":["../src/EdgeWorker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAwB3C,OAAO,KAAK,EAMX,iBAAiB,EACjB,gBAAgB,EAIhB,KAAK,EAGL,gBAAgB,EAChB,2BAA2B,EAO3B,MAAM,YAAY,CAAC;AAqBpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAgB/D,OAAO,EACN,gBAAgB,EAEhB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAoB,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAErE,MAAM,CAAC,OAAO,WAAW,UAAU;IAClC,EAAE,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAClC,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAC3B,IAAI,CAAC;IACR,IAAI,CAAC,CAAC,SAAS,MAAM,gBAAgB,EACpC,KAAK,EAAE,CAAC,EACR,GAAG,IAAI,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC;CACX;AAED;;;;;GAKG;AACH,qBAAa,UAAW,SAAQ,YAAY;IAC3C,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,YAAY,CAA4C;IAChE,OAAO,CAAC,oBAAoB,CAA+C;IAC3E,OAAO,CAAC,aAAa,CAAgD;IACrE,OAAO,CAAC,oBAAoB,CAAqC;IACjE,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,uBAAuB,CAA0B;IACzD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,yBAAyB,CAAkC;IACnE,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,aAAa,CAAC,CAAY;IAClC,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,2CAA2C;IACpC,gBAAgB,EAAE,gBAAgB,CAAC;IAC1C,OAAO,CAAC,UAAU,CAAa;IAC/B,OAAO,CAAC,kBAAkB,CAAK;IAC/B,4EAA4E;IAC5E,OAAO,CAAC,sBAAsB,CAAyB;gBAE3C,MAAM,EAAE,gBAAgB;IAsOpC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB5B;;OAEG;YACW,oBAAoB;IA0HlC;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAY9B;;;OAGG;IACH,OAAO,CAAC,uBAAuB;IAa/B;;;OAGG;IACH,OAAO,CAAC,aAAa;IAmBrB;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2C3B;;OAEG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIvC;;;;OAIG;YACW,yBAAyB;IAgIvC;;;OAGG;YACW,0BAA0B;IAoExC;;OAEG;YACW,yBAAyB;IAmCvC;;OAEG;YACW,yBAAyB;IAqDvC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;YACW,kBAAkB;IAoChC;;OAEG;YACW,gBAAgB;IAyD9B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAkC/B;;OAEG;IACH,OAAO,CAAC,SAAS;IAIjB;;OAEG;YACW,kBAAkB;IA+HhC;;OAEG;YACW,0BAA0B;IAiExC;;OAEG;YACW,yBAAyB;IAoEvC;;OAEG;IACH,OAAO,CAAC,WAAW;IAKnB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAO3B;;OAEG;YACW,aAAa;IAmD3B;;OAEG;YACW,4BAA4B;IAiC1C;;OAEG;IACH,OAAO,CAAC,2BAA2B;IAWnC;;;;;;;OAOG;YACW,wBAAwB;IAoFtC;;;;;OAKG;YACW,gCAAgC;IAqF9C;;;;;;;;;;;OAWG;YACW,qBAAqB;IAgXnC;;;;;;;OAOG;YACW,gBAAgB;IA+C9B;;;;;;;OAOG;YACW,iCAAiC;IAiE/C;;;;;OAKG;YACW,6BAA6B;IA0C3C;;;OAGG;YACW,4BAA4B;IAsN1C;;;;;;;;OAQG;YACW,+BAA+B;IAqD7C;;;;OAIG;YACW,qBAAqB;IAwCnC;;OAEG;YACW,mBAAmB;IAejC;;;OAGG;YACW,iBAAiB;IAiB/B;;OAEG;YACW,gBAAgB;IAa9B;;;;;;;;;OASG;IACH,OAAO,CAAC,yBAAyB;IA2FjC;;OAEG;YACW,+BAA+B;IA+L7C;;;;;;;OAOG;YACW,qBAAqB;IAkKnC;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,sBAAsB;IA6E9B;;;;;;;;OAQG;YACW,kBAAkB;IAoDhC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAUzB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAwB3B;;;;;;;;OAQG;YACW,mBAAmB;IAgGjC;;OAEG;IACH,OAAO,CAAC,wBAAwB;IAUhC;;;;;;;;;;;;;;;OAeG;YACW,mBAAmB;IAoCjC;;;;;;OAMG;YACW,gBAAgB;IAW9B;;;;OAIG;YACW,oBAAoB;IAmFlC;;;;;;;;OAQG;YACW,uBAAuB;IA2LrC;;OAEG;IACH,mBAAmB,IAAI,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAY3C;;;OAGG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG;IAKtC;;OAEG;IACG,cAAc,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAChD,WAAW,EAAE,MAAM,CAAC;QACpB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,mBAAmB,EAAE,MAAM,CAAC;KAC5B,CAAC;IAKF;;OAEG;IACH,aAAa,IAAI,MAAM;IAIvB;;OAEG;IACH,mBAAmB,IAAI,MAAM;IAI7B;;;;OAIG;YAEW,uBAAuB;IA+ErC;;OAEG;IAeH;;OAEG;YACW,WAAW;IAqBzB;;OAEG;IASH;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAY7B;;;;;OAKG;YACW,wBAAwB;IAwJtC;;OAEG;YACW,kBAAkB;IAuDhC;;;;;;OAMG;YACW,0BAA0B;IAwFxC;;OAEG;YACW,mBAAmB;IASjC;;OAEG;IACH,OAAO,CAAC,6BAA6B;IA8CrC;;OAEG;IACH,OAAO,CAAC,0BAA0B;IAoFlC;;OAEG;IACH,OAAO,CAAC,cAAc;IAsLtB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAoBzB;;;;;;;;;;;OAWG;YACW,kBAAkB;IAsChC;;;OAGG;YACW,cAAc;IAiB5B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAuB5B;;OAEG;YACW,qBAAqB;IAuGnC;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAmC/B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAgB3B;;;OAGG;YACW,oBAAoB;IAyClC;;OAEG;YACW,sBAAsB;IAsBpC;;OAEG;YACW,gCAAgC;IAW9C;;OAEG;YACW,kCAAkC;IA2ChD;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAuL9B;;;;;;;OAOG;IACH,OAAO,CAAC,6BAA6B;IAgBrC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA2D5B;;;;;;OAMG;IACH,OAAO,CAAC,8BAA8B;IAuBtC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkEzB;;OAEG;IACI,wBAAwB,CAC9B,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GAClB,GAAG,EAAE;IASR;;OAEG;YACW,kBAAkB;IAchC;;OAEG;YACW,kBAAkB;IAYhC;;OAEG;IACI,iBAAiB,IAAI,2BAA2B;IAoCvD;;OAEG;IACI,eAAe,CAAC,KAAK,EAAE,2BAA2B,GAAG,IAAI;IA6ChE;;OAEG;YACW,yBAAyB;IAwCvC;;OAEG;YACW,8BAA8B;IAwC5C;;;OAGG;YACW,+BAA+B;IAqE7C;;;OAGG;YACW,0BAA0B;IAoGxC;;;;;;;;;;;;;;;;;;OAkBG;YACW,8BAA8B;IA2E5C;;OAEG;YACW,gCAAgC;IA2G9C;;;;;;;;;;OAUG;IACG,kBAAkB,CACvB,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,gBAAgB,EAC5B,4BAA4B,EAAE,MAAM,EACpC,mBAAmB,EAAE,mBAAmB,EACxC,UAAU,EAAE,MAAM,EAClB,kBAAkB,GAAE,MAAW,EAC/B,YAAY,GAAE,OAAe,EAC7B,4BAA4B,GAAE,MAAM,EAAO,EAC3C,QAAQ,CAAC,EAAE,MAAM,EACjB,aAAa,CAAC,EAAE,MAAM,EACtB,gBAAgB,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC,IAAI,CAAC;IAgKhB;;OAEG;YACW,iCAAiC;IA6C/C;;OAEG;IACU,qBAAqB,CACjC,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,GAClB,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;IA0CxB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAgDxB;;OAEG;YACW,eAAe;CAsC7B"}
1
+ {"version":3,"file":"EdgeWorker.d.ts","sourceRoot":"","sources":["../src/EdgeWorker.ts"],"names":[],"mappings":"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"}
@@ -6,7 +6,7 @@ import { LinearClient } from "@linear/sdk";
6
6
  import { watch as chokidarWatch } from "chokidar";
7
7
  import { ClaudeRunner, createCyrusToolsServer, createImageToolsServer, createSoraToolsServer, getAllTools, getCoordinatorTools, getReadOnlyTools, getSafeTools, } from "cyrus-claude-runner";
8
8
  import { ConfigUpdater } from "cyrus-config-updater";
9
- import { CLIIssueTrackerService, CLIRPCServer, DEFAULT_PROXY_URL, isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueUnassignedWebhook, PersistenceManager, resolvePath, } from "cyrus-core";
9
+ import { CLIIssueTrackerService, CLIRPCServer, DEFAULT_PROXY_URL, isAgentSessionCreatedWebhook, isAgentSessionPromptedWebhook, isIssueAssignedWebhook, isIssueCommentMentionWebhook, isIssueNewCommentWebhook, isIssueTitleOrDescriptionUpdateWebhook, isIssueUnassignedWebhook, PersistenceManager, resolvePath, } from "cyrus-core";
10
10
  import { GeminiRunner } from "cyrus-gemini-runner";
11
11
  import { LinearEventTransport, LinearIssueTrackerService, } from "cyrus-linear-event-transport";
12
12
  import { fileTypeFromBuffer } from "file-type";
@@ -16,6 +16,7 @@ import { GitService } from "./GitService.js";
16
16
  import { ProcedureAnalyzer, } from "./procedures/index.js";
17
17
  import { RepositoryRouter, } from "./RepositoryRouter.js";
18
18
  import { SharedApplicationServer } from "./SharedApplicationServer.js";
19
+ import { UserAccessControl } from "./UserAccessControl.js";
19
20
  /**
20
21
  * Unified edge worker that **orchestrates**
21
22
  * capturing Linear webhooks,
@@ -43,6 +44,8 @@ export class EdgeWorker extends EventEmitter {
43
44
  activeWebhookCount = 0; // Track number of webhooks currently being processed
44
45
  /** Handler for AskUserQuestion tool invocations via Linear select signal */
45
46
  askUserQuestionHandler;
47
+ /** User access control for whitelisting/blacklisting Linear users */
48
+ userAccessControl;
46
49
  constructor(config) {
47
50
  super();
48
51
  this.config = config;
@@ -181,6 +184,14 @@ export class EdgeWorker extends EventEmitter {
181
184
  this.agentSessionManagers.set(repo.id, agentSessionManager);
182
185
  }
183
186
  }
187
+ // Initialize user access control with global and per-repository configs
188
+ const repoAccessConfigs = new Map();
189
+ for (const repo of config.repositories) {
190
+ if (repo.isActive !== false) {
191
+ repoAccessConfigs.set(repo.id, repo.userAccessControl);
192
+ }
193
+ }
194
+ this.userAccessControl = new UserAccessControl(config.userAccessControl, repoAccessConfigs);
184
195
  // Components will be initialized and registered in start() method before server starts
185
196
  }
186
197
  /**
@@ -620,6 +631,8 @@ export class EdgeWorker extends EventEmitter {
620
631
  defaultAllowedTools: parsedConfig.defaultAllowedTools || this.config.defaultAllowedTools,
621
632
  defaultDisallowedTools: parsedConfig.defaultDisallowedTools ||
622
633
  this.config.defaultDisallowedTools,
634
+ // Issue update trigger: use parsed value if explicitly set, otherwise keep current or default to true
635
+ issueUpdateTrigger: parsedConfig.issueUpdateTrigger ?? this.config.issueUpdateTrigger,
623
636
  };
624
637
  // Basic validation
625
638
  if (!Array.isArray(newConfig.repositories)) {
@@ -898,6 +911,10 @@ export class EdgeWorker extends EventEmitter {
898
911
  else if (isAgentSessionPromptedWebhook(webhook)) {
899
912
  await this.handleUserPromptedAgentActivity(webhook);
900
913
  }
914
+ else if (isIssueTitleOrDescriptionUpdateWebhook(webhook)) {
915
+ // Handle issue title/description/attachments updates - feed changes into active session
916
+ await this.handleIssueContentUpdate(webhook);
917
+ }
901
918
  else {
902
919
  if (process.env.CYRUS_WEBHOOK_DEBUG === "true") {
903
920
  console.log(`[handleWebhook] Unhandled webhook type: ${webhook.action}`);
@@ -936,6 +953,180 @@ export class EdgeWorker extends EventEmitter {
936
953
  // console.log('=== END WEBHOOK PAYLOAD ===')
937
954
  await this.handleIssueUnassigned(webhook.notification.issue, repository);
938
955
  }
956
+ /**
957
+ * Handle issue content update webhook (title, description, or attachments).
958
+ *
959
+ * When the title, description, or attachments of an issue are updated, this handler feeds
960
+ * the changes into any active session for that issue, allowing the AI to
961
+ * compare old vs new values and decide whether to take action.
962
+ *
963
+ * The prompt uses XML-style formatting to clearly show what changed:
964
+ * - <issue_update> wrapper with timestamp and issue identifier
965
+ * - <title_change> with <old_title> and <new_title> if title changed
966
+ * - <description_change> with <old_description> and <new_description> if description changed
967
+ * - <attachments_change> with <old_attachments> and <new_attachments> if attachments changed
968
+ * - <guidance> section instructing the agent to evaluate whether changes affect its work
969
+ *
970
+ * @see https://studio.apollographql.com/public/Linear-Webhooks/variant/current/schema/reference/objects/EntityWebhookPayload
971
+ * @see https://studio.apollographql.com/public/Linear-Webhooks/variant/current/schema/reference/objects/IssueWebhookPayload
972
+ * @see https://studio.apollographql.com/public/Linear-Webhooks/variant/current/schema/reference/unions/DataWebhookPayload
973
+ */
974
+ async handleIssueContentUpdate(webhook) {
975
+ // Check if issue update trigger is enabled (defaults to true if not set)
976
+ if (this.config.issueUpdateTrigger === false) {
977
+ if (process.env.CYRUS_WEBHOOK_DEBUG === "true") {
978
+ console.log("[EdgeWorker] Issue update trigger is disabled, skipping issue content update");
979
+ }
980
+ return;
981
+ }
982
+ const issueData = webhook.data;
983
+ const issueId = issueData.id;
984
+ const issueIdentifier = issueData.identifier;
985
+ const updatedFrom = webhook.updatedFrom;
986
+ if (!updatedFrom) {
987
+ console.warn(`[EdgeWorker] Issue update webhook for ${issueIdentifier} has no updatedFrom data`);
988
+ return;
989
+ }
990
+ // Get cached repository (updates should only be processed for issues with active sessions)
991
+ const repository = this.getCachedRepository(issueId);
992
+ if (!repository) {
993
+ if (process.env.CYRUS_WEBHOOK_DEBUG === "true") {
994
+ console.log(`[EdgeWorker] No cached repository for issue update webhook ${issueIdentifier} (no active sessions to notify)`);
995
+ }
996
+ return;
997
+ }
998
+ // Determine what changed for logging
999
+ const changedFields = [];
1000
+ if ("title" in updatedFrom)
1001
+ changedFields.push("title");
1002
+ if ("description" in updatedFrom)
1003
+ changedFields.push("description");
1004
+ if ("attachments" in updatedFrom)
1005
+ changedFields.push("attachments");
1006
+ console.log(`[EdgeWorker] Handling issue content update: ${issueIdentifier} (changed: ${changedFields.join(", ")})`);
1007
+ // Get agent session manager for this repository
1008
+ const agentSessionManager = this.agentSessionManagers.get(repository.id);
1009
+ if (!agentSessionManager) {
1010
+ console.log(`[EdgeWorker] No agent session manager for repository ${repository.id}`);
1011
+ return;
1012
+ }
1013
+ // Find session(s) for this issue (may be running or paused between subroutines)
1014
+ const sessions = agentSessionManager.getSessionsByIssueId(issueId);
1015
+ if (sessions.length === 0) {
1016
+ if (process.env.CYRUS_WEBHOOK_DEBUG === "true") {
1017
+ console.log(`[EdgeWorker] No sessions found for issue ${issueIdentifier} to receive update`);
1018
+ }
1019
+ return;
1020
+ }
1021
+ // Process attachments from the updated description if description changed
1022
+ let attachmentManifest = "";
1023
+ if ("description" in updatedFrom && issueData.description) {
1024
+ const firstSession = sessions[0];
1025
+ if (!firstSession) {
1026
+ console.log(`[EdgeWorker] No sessions found for issue ${issueIdentifier}`);
1027
+ return;
1028
+ }
1029
+ const workspaceFolderName = basename(firstSession.workspace.path);
1030
+ const attachmentsDir = join(this.cyrusHome, workspaceFolderName, "attachments");
1031
+ try {
1032
+ // Ensure directory exists
1033
+ await mkdir(attachmentsDir, { recursive: true });
1034
+ // Count existing attachments
1035
+ const existingFiles = await readdir(attachmentsDir).catch(() => []);
1036
+ const existingAttachmentCount = existingFiles.filter((file) => file.startsWith("attachment_") || file.startsWith("image_")).length;
1037
+ // Download attachments from the new description
1038
+ const downloadResult = await this.downloadCommentAttachments(issueData.description, attachmentsDir, repository.linearToken, existingAttachmentCount);
1039
+ if (downloadResult.totalNewAttachments > 0) {
1040
+ attachmentManifest =
1041
+ this.generateNewAttachmentManifest(downloadResult);
1042
+ console.log(`[EdgeWorker] Downloaded ${downloadResult.totalNewAttachments} attachments from updated description`);
1043
+ }
1044
+ }
1045
+ catch (error) {
1046
+ console.error("[EdgeWorker] Failed to process attachments from updated description:", error);
1047
+ }
1048
+ }
1049
+ // Build the XML-formatted prompt showing old vs new values
1050
+ const promptBody = this.buildIssueUpdatePrompt(issueIdentifier, issueData, updatedFrom);
1051
+ // Feed the update into each active session
1052
+ for (const session of sessions) {
1053
+ const linearAgentActivitySessionId = session.linearAgentActivitySessionId;
1054
+ // Check if runner is actively running and supports streaming input
1055
+ const existingRunner = session.agentRunner;
1056
+ const isRunning = existingRunner?.isRunning() || false;
1057
+ // Combine prompt body with attachment manifest
1058
+ let fullPrompt = promptBody;
1059
+ if (attachmentManifest) {
1060
+ fullPrompt = `${promptBody}\n\n${attachmentManifest}`;
1061
+ }
1062
+ if (isRunning &&
1063
+ existingRunner?.supportsStreamingInput &&
1064
+ existingRunner.addStreamMessage) {
1065
+ // Add to existing stream
1066
+ console.log(`[EdgeWorker] Adding issue update to existing stream for ${linearAgentActivitySessionId}`);
1067
+ existingRunner.addStreamMessage(fullPrompt);
1068
+ }
1069
+ else if (isRunning) {
1070
+ // Runner is running but doesn't support streaming input - log and skip
1071
+ console.log(`[EdgeWorker] Session ${linearAgentActivitySessionId} is running but doesn't support streaming input, skipping issue update`);
1072
+ }
1073
+ else {
1074
+ // Session exists but runner is not running - resume with the update
1075
+ console.log(`[EdgeWorker] Resuming session ${linearAgentActivitySessionId} with issue update`);
1076
+ await this.handlePromptWithStreamingCheck(session, repository, linearAgentActivitySessionId, agentSessionManager, promptBody, attachmentManifest, false, // Not a new session
1077
+ [], // No additional allowed directories
1078
+ "issue content update", undefined, // No comment author
1079
+ undefined);
1080
+ }
1081
+ }
1082
+ }
1083
+ /**
1084
+ * Build an XML-formatted prompt for issue content updates (title, description, attachments).
1085
+ *
1086
+ * The prompt clearly shows what fields changed by comparing old vs new values,
1087
+ * and includes guidance for the agent to evaluate whether these changes affect
1088
+ * its current implementation or action plan.
1089
+ */
1090
+ buildIssueUpdatePrompt(issueIdentifier, issueData, updatedFrom) {
1091
+ const timestamp = new Date().toISOString();
1092
+ const parts = [];
1093
+ parts.push(`<issue_update>`);
1094
+ parts.push(` <identifier>${issueIdentifier}</identifier>`);
1095
+ parts.push(` <timestamp>${timestamp}</timestamp>`);
1096
+ // Add title change if title was updated
1097
+ if ("title" in updatedFrom) {
1098
+ parts.push(` <title_change>`);
1099
+ parts.push(` <old_title>${updatedFrom.title ?? ""}</old_title>`);
1100
+ parts.push(` <new_title>${issueData.title}</new_title>`);
1101
+ parts.push(` </title_change>`);
1102
+ }
1103
+ // Add description change if description was updated
1104
+ if ("description" in updatedFrom) {
1105
+ parts.push(` <description_change>`);
1106
+ parts.push(` <old_description>${updatedFrom.description ?? ""}</old_description>`);
1107
+ parts.push(` <new_description>${issueData.description ?? ""}</new_description>`);
1108
+ parts.push(` </description_change>`);
1109
+ }
1110
+ // Add attachments change if attachments were updated
1111
+ if ("attachments" in updatedFrom) {
1112
+ parts.push(` <attachments_change>`);
1113
+ parts.push(` <old_attachments>${JSON.stringify(updatedFrom.attachments ?? null)}</old_attachments>`);
1114
+ parts.push(` <new_attachments>${JSON.stringify(issueData.attachments ?? null)}</new_attachments>`);
1115
+ parts.push(` </attachments_change>`);
1116
+ }
1117
+ parts.push(`</issue_update>`);
1118
+ // Add guidance for the agent on how to respond to this update
1119
+ parts.push(``);
1120
+ parts.push(`<guidance>`);
1121
+ parts.push(` The issue has been updated while you are working on it. Please evaluate whether these changes`);
1122
+ parts.push(` affect your current implementation or action plan. Consider the following:`);
1123
+ parts.push(` - Does the updated content change the requirements or scope of your work?`);
1124
+ parts.push(` - Are there new details, clarifications, or attachments that should inform your approach?`);
1125
+ parts.push(` - Should you adjust your implementation strategy based on this update?`);
1126
+ parts.push(` If the changes are relevant, incorporate them into your work. If not, you may continue as planned.`);
1127
+ parts.push(`</guidance>`);
1128
+ return parts.join("\n");
1129
+ }
939
1130
  /**
940
1131
  * Get issue tracker for a workspace by finding first repository with that workspace ID
941
1132
  */
@@ -1050,6 +1241,13 @@ export class EdgeWorker extends EventEmitter {
1050
1241
  console.warn("[EdgeWorker] Agent session created webhook missing issue");
1051
1242
  return;
1052
1243
  }
1244
+ // User access control check
1245
+ const accessResult = this.checkUserAccess(webhook, repository);
1246
+ if (!accessResult.allowed) {
1247
+ console.log(`[EdgeWorker] User ${accessResult.userName} blocked from delegating: ${accessResult.reason}`);
1248
+ await this.handleBlockedUser(webhook, repository, accessResult.reason);
1249
+ return;
1250
+ }
1053
1251
  console.log(`[EdgeWorker] Handling agent session created: ${webhook.agentSession.issue.identifier}`);
1054
1252
  const { agentSession, guidance } = webhook;
1055
1253
  const commentBody = agentSession.comment?.body;
@@ -1565,6 +1763,13 @@ export class EdgeWorker extends EventEmitter {
1565
1763
  console.warn(`[EdgeWorker] No cached repository found for prompted webhook ${agentSessionId}`);
1566
1764
  return;
1567
1765
  }
1766
+ // User access control check for mid-session prompts
1767
+ const accessResult = this.checkUserAccess(webhook, repository);
1768
+ if (!accessResult.allowed) {
1769
+ console.log(`[EdgeWorker] User ${accessResult.userName} blocked from prompting: ${accessResult.reason}`);
1770
+ await this.handleBlockedUser(webhook, repository, accessResult.reason);
1771
+ return;
1772
+ }
1568
1773
  await this.handleNormalPromptedActivity(webhook, repository);
1569
1774
  }
1570
1775
  /**
@@ -2332,9 +2537,8 @@ ${reply.body}
2332
2537
  async buildIssueContextPrompt(issue, repository, newComment, attachmentManifest = "", guidance) {
2333
2538
  console.log(`[EdgeWorker] buildIssueContextPrompt called for issue ${issue.identifier}${newComment ? " with new comment" : ""}`);
2334
2539
  try {
2335
- // Use custom template if provided (repository-specific takes precedence)
2336
- let templatePath = repository.promptTemplatePath ||
2337
- this.config.features?.promptTemplatePath;
2540
+ // Use custom template if provided (repository-specific)
2541
+ let templatePath = repository.promptTemplatePath;
2338
2542
  // If no custom template, use the standard issue assigned user prompt template
2339
2543
  if (!templatePath) {
2340
2544
  const __filename = fileURLToPath(import.meta.url);
@@ -3645,6 +3849,69 @@ ${input.userComment}
3645
3849
  }
3646
3850
  return agentSessionManager.getSessionsByIssueId(issueId);
3647
3851
  }
3852
+ // ========================================================================
3853
+ // User Access Control
3854
+ // ========================================================================
3855
+ /**
3856
+ * Check if the user who triggered the webhook is allowed to interact.
3857
+ * @param webhook The webhook containing user information
3858
+ * @param repository The repository configuration
3859
+ * @returns Access check result with allowed status and user name
3860
+ */
3861
+ checkUserAccess(webhook, repository) {
3862
+ const creator = webhook.agentSession.creator;
3863
+ const userId = creator?.id;
3864
+ const userEmail = creator?.email;
3865
+ const userName = creator?.name || userId || "Unknown";
3866
+ const result = this.userAccessControl.checkAccess(userId, userEmail, repository.id);
3867
+ if (!result.allowed) {
3868
+ return { allowed: false, reason: result.reason, userName };
3869
+ }
3870
+ return { allowed: true };
3871
+ }
3872
+ /**
3873
+ * Handle blocked user according to configured behavior.
3874
+ * Posts a response activity to end the session.
3875
+ * @param webhook The webhook that triggered the blocked access
3876
+ * @param repository The repository configuration
3877
+ * @param _reason The reason for blocking (for logging)
3878
+ */
3879
+ async handleBlockedUser(webhook, repository, _reason) {
3880
+ const issueTracker = this.issueTrackers.get(repository.id);
3881
+ const agentSessionId = webhook.agentSession.id;
3882
+ const behavior = this.userAccessControl.getBlockBehavior(repository.id);
3883
+ if (!issueTracker) {
3884
+ return;
3885
+ }
3886
+ if (behavior === "comment") {
3887
+ // Get user info for templating
3888
+ const creator = webhook.agentSession.creator;
3889
+ const userName = creator?.name || "User";
3890
+ const userId = creator?.id || "";
3891
+ // Get the message template and replace variables
3892
+ // Supported variables:
3893
+ // - {{userName}} - The user's display name
3894
+ // - {{userId}} - The user's Linear ID
3895
+ let message = this.userAccessControl.getBlockMessage(repository.id);
3896
+ message = message
3897
+ .replace(/\{\{userName\}\}/g, userName)
3898
+ .replace(/\{\{userId\}\}/g, userId);
3899
+ try {
3900
+ await issueTracker.createAgentActivity({
3901
+ agentSessionId,
3902
+ content: {
3903
+ type: "response",
3904
+ body: message,
3905
+ },
3906
+ });
3907
+ }
3908
+ catch (error) {
3909
+ console.error("[EdgeWorker] Failed to post blocked user message:", error);
3910
+ }
3911
+ }
3912
+ // For "silent" behavior, we don't post any activity.
3913
+ // The session will remain in "Working" state until manually stopped or timed out.
3914
+ }
3648
3915
  /**
3649
3916
  * Load persisted EdgeWorker state for all repositories
3650
3917
  */