Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit 5a19ec3

Browse files
authored
Merge pull request #36 from closedloop-ai/FEAT-95
FEAT-95: Fix approval policy threshold bug and remove mcp-auth endpoint
2 parents d7e3de5 + bde7a5a commit 5a19ec3

6 files changed

Lines changed: 307 additions & 146 deletions

File tree

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "desktop",
3-
"version": "0.6.1",
3+
"version": "0.7.0",
44
"description": "ClosedLoop Desktop",
55
"author": "ClosedLoop AI <support@closedloop.ai>",
66
"private": true,

apps/desktop/src/main/app.ts

Lines changed: 13 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {
2828
SymphonyDirNotConfiguredError
2929
} from "../server/operations/symphony-utils.js";
3030
import { seedReposConfig } from "./seed-repos-config.js";
31+
import { SUPPORTED_OPERATION_IDS, resolveOperationId } from "./approval-operations.js";
32+
import { shouldAutoApprove } from "./approval-policy.js";
3133
import { ActivityLogStore } from "./activity-log-store.js";
3234
import { ApprovalStore } from "./approval-store.js";
3335
import { JobStore, isTerminalJobStatus } from "./job-store.js";
@@ -131,7 +133,7 @@ export class DesktopApplication {
131133
getMaxInFlightCommands: () => MAX_IN_FLIGHT_COMMANDS,
132134
machineName: os.hostname(),
133135
pluginVersion: DESKTOP_GATEWAY_VERSION,
134-
supportedOperations: SUPPORTED_OPERATION_IDS,
136+
supportedOperations: [...SUPPORTED_OPERATION_IDS],
135137
onStatusChange: (status) => this.onCloudSocketStatus(status),
136138
onHelloAck: (event) => {
137139
if (event.resumeFromSequence) {
@@ -525,7 +527,11 @@ export class DesktopApplication {
525527

526528
const operationId = resolveOperationId(request.path);
527529
if (!operationId) {
528-
return { allow: true };
530+
return {
531+
allow: false,
532+
statusCode: 403,
533+
payload: { error: `Unmapped operation: ${request.path}` }
534+
};
529535
}
530536

531537
const settings = this.settingsStore.getAll();
@@ -547,11 +553,13 @@ export class DesktopApplication {
547553

548554
const configuredTier = (settings.autoApprovalRules[operationId] ??
549555
settings.defaultApprovalTier) as RiskTier;
550-
const tier: RiskTier = request.forceApproval ? "high" : configuredTier;
551-
if (tier === "auto" && !request.forceApproval) {
556+
if (configuredTier === "auto" && !request.forceApproval) {
557+
return { allow: true };
558+
}
559+
const manualTier: Exclude<RiskTier, "auto"> = configuredTier === "auto" ? "high" : configuredTier;
560+
if (shouldAutoApprove(operationId, manualTier, request.forceApproval ?? false)) {
552561
return { allow: true };
553562
}
554-
const manualTier: Exclude<RiskTier, "auto"> = tier === "auto" ? "high" : tier;
555563

556564
const reason =
557565
request.approvalReason?.trim() ||
@@ -1019,140 +1027,6 @@ export class DesktopApplication {
10191027
}
10201028
}
10211029

1022-
const SUPPORTED_OPERATION_IDS = [
1023-
"symphony_launch",
1024-
"symphony_loop",
1025-
"symphony_loop_kill",
1026-
"symphony_plan_loop",
1027-
"symphony_status",
1028-
"symphony_kill",
1029-
"symphony_chat",
1030-
"symphony_comment_chat",
1031-
"symphony_commit_message",
1032-
"symphony_sessions",
1033-
"symphony_plan",
1034-
"symphony_judges",
1035-
"symphony_logs",
1036-
"symphony_chat_history",
1037-
"terminal_chat",
1038-
"ticket_chat",
1039-
"run_viewer_chat",
1040-
"codex_review",
1041-
"codex_argue",
1042-
"git_action",
1043-
"git_pr",
1044-
"health_check",
1045-
"repos_config",
1046-
"deploy",
1047-
"learnings",
1048-
"filesystem"
1049-
];
1050-
1051-
function resolveOperationId(pathname: string): string | null {
1052-
if (!pathname.startsWith("/api/engineer/")) {
1053-
return null;
1054-
}
1055-
1056-
if (pathname === "/api/engineer/symphony/launch") {
1057-
return "symphony_launch";
1058-
}
1059-
if (pathname === "/api/engineer/symphony/loop") {
1060-
return "symphony_loop";
1061-
}
1062-
if (pathname === "/api/engineer/symphony/loop/kill") {
1063-
return "symphony_loop_kill";
1064-
}
1065-
if (pathname.startsWith("/api/engineer/symphony/plan-loop/")) {
1066-
return "symphony_plan_loop";
1067-
}
1068-
if (pathname.startsWith("/api/engineer/symphony/status/")) {
1069-
return "symphony_status";
1070-
}
1071-
if (pathname === "/api/engineer/symphony/kill") {
1072-
return "symphony_kill";
1073-
}
1074-
if (pathname.startsWith("/api/engineer/symphony/chat/")) {
1075-
return "symphony_chat";
1076-
}
1077-
if (pathname.startsWith("/api/engineer/symphony/comment-chat/")) {
1078-
return "symphony_comment_chat";
1079-
}
1080-
if (pathname.startsWith("/api/engineer/symphony/commit-message/")) {
1081-
return "symphony_commit_message";
1082-
}
1083-
if (pathname === "/api/engineer/symphony/sessions") {
1084-
return "symphony_sessions";
1085-
}
1086-
if (pathname.startsWith("/api/engineer/symphony/plan/")) {
1087-
return "symphony_plan";
1088-
}
1089-
if (pathname.startsWith("/api/engineer/symphony/judges/")) {
1090-
return "symphony_judges";
1091-
}
1092-
if (pathname.startsWith("/api/engineer/symphony/logs/")) {
1093-
return "symphony_logs";
1094-
}
1095-
if (pathname.startsWith("/api/engineer/symphony/chat-history/")) {
1096-
return "symphony_chat_history";
1097-
}
1098-
if (pathname.startsWith("/api/engineer/symphony/pending-learnings")) {
1099-
return "learnings";
1100-
}
1101-
if (pathname.startsWith("/api/engineer/symphony/process-learnings")) {
1102-
return "learnings";
1103-
}
1104-
if (pathname.startsWith("/api/engineer/symphony/process-all-learnings")) {
1105-
return "learnings";
1106-
}
1107-
if (pathname === "/api/engineer/terminal-chat") {
1108-
return "terminal_chat";
1109-
}
1110-
if (pathname === "/api/engineer/ticket-chat") {
1111-
return "ticket_chat";
1112-
}
1113-
if (pathname === "/api/engineer/run-viewer-chat") {
1114-
return "run_viewer_chat";
1115-
}
1116-
if (pathname.startsWith("/api/engineer/codex/argue/")) {
1117-
return "codex_argue";
1118-
}
1119-
if (pathname.startsWith("/api/engineer/codex/")) {
1120-
return "codex_review";
1121-
}
1122-
if (pathname.startsWith("/api/engineer/git/pr") || pathname === "/api/engineer/git/user") {
1123-
return "git_pr";
1124-
}
1125-
if (pathname.startsWith("/api/engineer/git")) {
1126-
return "git_action";
1127-
}
1128-
if (pathname === "/api/engineer/health-check") {
1129-
return "health_check";
1130-
}
1131-
if (pathname === "/api/engineer/repos") {
1132-
return "repos_config";
1133-
}
1134-
if (pathname.startsWith("/api/engineer/deploy")) {
1135-
return "deploy";
1136-
}
1137-
if (pathname === "/api/engineer/learnings") {
1138-
return "learnings";
1139-
}
1140-
if (pathname.startsWith("/api/engineer/work-directory/")) {
1141-
return "filesystem";
1142-
}
1143-
if (pathname.startsWith("/api/engineer/symphony/sessions/")) {
1144-
return "symphony_sessions";
1145-
}
1146-
if (
1147-
pathname === "/api/engineer/directories" ||
1148-
pathname === "/api/engineer/files/search" ||
1149-
pathname.startsWith("/api/engineer/run-viewer-extract")
1150-
) {
1151-
return "filesystem";
1152-
}
1153-
1154-
return null;
1155-
}
11561030

11571031
const APPROVAL_TIMEOUT_MS = 120_000;
11581032
const MAX_IN_FLIGHT_COMMANDS = 2;
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/**
2+
* Electron-free operation catalog for the approval system.
3+
* Extracted from app.ts so it can be imported in plain Node tests.
4+
*/
5+
6+
export const SUPPORTED_OPERATION_IDS = [
7+
"symphony_launch",
8+
"symphony_loop",
9+
"symphony_loop_kill",
10+
"symphony_plan_loop",
11+
"symphony_status",
12+
"symphony_kill",
13+
"symphony_chat",
14+
"symphony_comment_chat",
15+
"symphony_commit_message",
16+
"symphony_sessions",
17+
"symphony_plan",
18+
"symphony_judges",
19+
"symphony_logs",
20+
"symphony_chat_history",
21+
"terminal_chat",
22+
"ticket_chat",
23+
"run_viewer_chat",
24+
"codex_review",
25+
"codex_argue",
26+
"git_action",
27+
"git_pr",
28+
"health_check",
29+
"repos_config",
30+
"deploy",
31+
"learnings",
32+
"filesystem"
33+
] as const;
34+
35+
export type OperationId = (typeof SUPPORTED_OPERATION_IDS)[number];
36+
37+
export function resolveOperationId(pathname: string): string | null {
38+
if (!pathname.startsWith("/api/engineer/")) {
39+
return null;
40+
}
41+
42+
if (pathname === "/api/engineer/symphony/launch") {
43+
return "symphony_launch";
44+
}
45+
if (pathname === "/api/engineer/symphony/loop") {
46+
return "symphony_loop";
47+
}
48+
if (pathname === "/api/engineer/symphony/loop/kill") {
49+
return "symphony_loop_kill";
50+
}
51+
if (pathname.startsWith("/api/engineer/symphony/plan-loop/")) {
52+
return "symphony_plan_loop";
53+
}
54+
if (pathname === "/api/engineer/symphony/status" || pathname.startsWith("/api/engineer/symphony/status/")) {
55+
return "symphony_status";
56+
}
57+
if (pathname === "/api/engineer/symphony/kill") {
58+
return "symphony_kill";
59+
}
60+
if (pathname.startsWith("/api/engineer/symphony/chat/")) {
61+
return "symphony_chat";
62+
}
63+
if (pathname.startsWith("/api/engineer/symphony/comment-chat/")) {
64+
return "symphony_comment_chat";
65+
}
66+
if (pathname.startsWith("/api/engineer/symphony/commit-message/")) {
67+
return "symphony_commit_message";
68+
}
69+
if (pathname === "/api/engineer/symphony/sessions") {
70+
return "symphony_sessions";
71+
}
72+
if (pathname.startsWith("/api/engineer/symphony/plan/")) {
73+
return "symphony_plan";
74+
}
75+
if (pathname.startsWith("/api/engineer/symphony/judges/")) {
76+
return "symphony_judges";
77+
}
78+
if (pathname.startsWith("/api/engineer/symphony/logs/")) {
79+
return "symphony_logs";
80+
}
81+
if (pathname.startsWith("/api/engineer/symphony/chat-history/")) {
82+
return "symphony_chat_history";
83+
}
84+
if (pathname.startsWith("/api/engineer/symphony/pending-learnings")) {
85+
return "learnings";
86+
}
87+
if (pathname.startsWith("/api/engineer/symphony/process-learnings")) {
88+
return "learnings";
89+
}
90+
if (pathname.startsWith("/api/engineer/symphony/process-all-learnings")) {
91+
return "learnings";
92+
}
93+
if (pathname.startsWith("/api/engineer/symphony/extract-learnings")) {
94+
return "learnings";
95+
}
96+
if (pathname.startsWith("/api/engineer/symphony/learnings-status/")) {
97+
return "learnings";
98+
}
99+
if (pathname === "/api/engineer/symphony/record-learning-use") {
100+
return "learnings";
101+
}
102+
if (pathname === "/api/engineer/terminal-chat") {
103+
return "terminal_chat";
104+
}
105+
if (pathname === "/api/engineer/ticket-chat") {
106+
return "ticket_chat";
107+
}
108+
if (pathname === "/api/engineer/run-viewer-chat") {
109+
return "run_viewer_chat";
110+
}
111+
if (pathname.startsWith("/api/engineer/codex/argue/")) {
112+
return "codex_argue";
113+
}
114+
if (pathname.startsWith("/api/engineer/codex/")) {
115+
return "codex_review";
116+
}
117+
if (pathname.startsWith("/api/engineer/git/pr") || pathname === "/api/engineer/git/user") {
118+
return "git_pr";
119+
}
120+
if (pathname.startsWith("/api/engineer/git")) {
121+
return "git_action";
122+
}
123+
if (pathname === "/api/engineer/health-check") {
124+
return "health_check";
125+
}
126+
if (pathname === "/api/engineer/repos") {
127+
return "repos_config";
128+
}
129+
if (pathname.startsWith("/api/engineer/deploy")) {
130+
return "deploy";
131+
}
132+
if (pathname === "/api/engineer/learnings") {
133+
return "learnings";
134+
}
135+
if (pathname.startsWith("/api/engineer/work-directory/")) {
136+
return "filesystem";
137+
}
138+
if (pathname.startsWith("/api/engineer/symphony/sessions/")) {
139+
return "symphony_sessions";
140+
}
141+
if (pathname.startsWith("/api/engineer/symphony/attachments/")) {
142+
return "filesystem";
143+
}
144+
if (pathname.startsWith("/api/engineer/symphony/upload/")) {
145+
return "filesystem";
146+
}
147+
if (pathname === "/api/engineer/version") {
148+
return "health_check";
149+
}
150+
if (
151+
pathname === "/api/engineer/directories" ||
152+
pathname === "/api/engineer/files/search" ||
153+
pathname.startsWith("/api/engineer/run-viewer-extract")
154+
) {
155+
return "filesystem";
156+
}
157+
158+
return null;
159+
}

0 commit comments

Comments
 (0)