POST /api/index — body: {"root_path": "/abs/path", "project_name": "..."} */
| 1144 | |
| 1145 | /* POST /api/index — body: {"root_path": "/abs/path", "project_name": "..."} */ |
| 1146 | static void handle_index_start(cbm_http_server_t *server, cbm_http_conn_t *c, |
| 1147 | const cbm_http_req_t *req) { |
| 1148 | if (!server || !server->index_executor) { |
| 1149 | cbm_http_replyf(c, 503, g_cors_json, |
| 1150 | "{\"error\":\"daemon index coordinator unavailable\"}"); |
| 1151 | return; |
| 1152 | } |
| 1153 | if (req->body_len == 0 || req->body_len > 4096) { |
| 1154 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"invalid body\"}"); |
| 1155 | return; |
| 1156 | } |
| 1157 | |
| 1158 | yyjson_doc *doc = yyjson_read(req->body, req->body_len, 0); |
| 1159 | if (!doc) { |
| 1160 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"invalid json\"}"); |
| 1161 | return; |
| 1162 | } |
| 1163 | yyjson_val *root = yyjson_doc_get_root(doc); |
| 1164 | yyjson_val *v_path = yyjson_obj_get(root, "root_path"); |
| 1165 | if (!v_path || !yyjson_is_str(v_path)) { |
| 1166 | yyjson_doc_free(doc); |
| 1167 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"missing root_path\"}"); |
| 1168 | return; |
| 1169 | } |
| 1170 | const char *rpath = yyjson_get_str(v_path); |
| 1171 | yyjson_val *v_project_name = yyjson_obj_get(root, "project_name"); |
| 1172 | const char *project_name = yyjson_is_str(v_project_name) ? yyjson_get_str(v_project_name) : ""; |
| 1173 | |
| 1174 | /* Check path exists */ |
| 1175 | if (!cbm_is_dir(rpath)) { |
| 1176 | yyjson_doc_free(doc); |
| 1177 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"directory not found\"}"); |
| 1178 | return; |
| 1179 | } |
| 1180 | |
| 1181 | /* Same workspace boundary the MCP indexing tool applies, through the same |
| 1182 | * function. This route used to check only that the path was a directory, so |
| 1183 | * it accepted roots the MCP path refused — an operator's boundary held on one |
| 1184 | * entry point and not the other. Canonicalize first: the policy is defined |
| 1185 | * over resolved paths, and a symlink would otherwise launder the verdict. */ |
| 1186 | char canonical_root[4096]; |
| 1187 | char boundary_err[1024]; |
| 1188 | if (!cbm_canonical_path(rpath, canonical_root, sizeof(canonical_root))) { |
| 1189 | yyjson_doc_free(doc); |
| 1190 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"cannot resolve root_path\"}"); |
| 1191 | return; |
| 1192 | } |
| 1193 | if (!cbm_workspace_root_allowed(canonical_root, cbm_workspace_home_dir(), |
| 1194 | cbm_workspace_cache_dir(), getenv("CBM_ALLOWED_ROOT"), |
| 1195 | boundary_err, sizeof(boundary_err))) { |
| 1196 | yyjson_doc_free(doc); |
| 1197 | char escaped[1024]; |
| 1198 | cbm_json_escape(escaped, (int)sizeof(escaped), boundary_err); |
| 1199 | cbm_http_replyf(c, 403, g_cors_json, "{\"error\":\"%s\"}", escaped); |
| 1200 | return; |
| 1201 | } |
| 1202 | |
| 1203 | /* Find free job slot */ |
no test coverage detected