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.
- package/dist/EdgeWorker.d.ts +44 -0
- package/dist/EdgeWorker.d.ts.map +1 -1
- package/dist/EdgeWorker.js +271 -4
- package/dist/EdgeWorker.js.map +1 -1
- package/dist/RepositoryRouter.d.ts +4 -0
- package/dist/RepositoryRouter.d.ts.map +1 -1
- package/dist/RepositoryRouter.js +29 -6
- package/dist/RepositoryRouter.js.map +1 -1
- package/dist/SharedApplicationServer.js +1 -1
- package/dist/SharedApplicationServer.js.map +1 -1
- package/dist/UserAccessControl.d.ts +69 -0
- package/dist/UserAccessControl.d.ts.map +1 -0
- package/dist/UserAccessControl.js +171 -0
- package/dist/UserAccessControl.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/EdgeWorker.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/EdgeWorker.d.ts.map
CHANGED
|
@@ -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,
|
|
1
|
+
{"version":3,"file":"EdgeWorker.d.ts","sourceRoot":"","sources":["../src/EdgeWorker.ts"],"names":[],"mappings":"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"}
|
package/dist/EdgeWorker.js
CHANGED
|
@@ -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
|
|
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
|
*/
|