POST /api/index — body: {"root_path": "/abs/path", "project_name": "..."} */
| 1016 | |
| 1017 | /* POST /api/index — body: {"root_path": "/abs/path", "project_name": "..."} */ |
| 1018 | static void handle_index_start(cbm_http_server_t *server, cbm_http_conn_t *c, |
| 1019 | const cbm_http_req_t *req) { |
| 1020 | if (!server || !server->index_executor) { |
| 1021 | cbm_http_replyf(c, 503, g_cors_json, |
| 1022 | "{\"error\":\"daemon index coordinator unavailable\"}"); |
| 1023 | return; |
| 1024 | } |
| 1025 | if (req->body_len == 0 || req->body_len > 4096) { |
| 1026 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"invalid body\"}"); |
| 1027 | return; |
| 1028 | } |
| 1029 | |
| 1030 | yyjson_doc *doc = yyjson_read(req->body, req->body_len, 0); |
| 1031 | if (!doc) { |
| 1032 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"invalid json\"}"); |
| 1033 | return; |
| 1034 | } |
| 1035 | yyjson_val *root = yyjson_doc_get_root(doc); |
| 1036 | yyjson_val *v_path = yyjson_obj_get(root, "root_path"); |
| 1037 | if (!v_path || !yyjson_is_str(v_path)) { |
| 1038 | yyjson_doc_free(doc); |
| 1039 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"missing root_path\"}"); |
| 1040 | return; |
| 1041 | } |
| 1042 | const char *rpath = yyjson_get_str(v_path); |
| 1043 | yyjson_val *v_project_name = yyjson_obj_get(root, "project_name"); |
| 1044 | const char *project_name = yyjson_is_str(v_project_name) ? yyjson_get_str(v_project_name) : ""; |
| 1045 | |
| 1046 | /* Check path exists */ |
| 1047 | if (!cbm_is_dir(rpath)) { |
| 1048 | yyjson_doc_free(doc); |
| 1049 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"directory not found\"}"); |
| 1050 | return; |
| 1051 | } |
| 1052 | |
| 1053 | /* Same workspace boundary the MCP indexing tool applies, through the same |
| 1054 | * function. This route used to check only that the path was a directory, so |
| 1055 | * it accepted roots the MCP path refused — an operator's boundary held on one |
| 1056 | * entry point and not the other. Canonicalize first: the policy is defined |
| 1057 | * over resolved paths, and a symlink would otherwise launder the verdict. */ |
| 1058 | char canonical_root[4096]; |
| 1059 | char boundary_err[1024]; |
| 1060 | if (!cbm_canonical_path(rpath, canonical_root, sizeof(canonical_root))) { |
| 1061 | yyjson_doc_free(doc); |
| 1062 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"cannot resolve root_path\"}"); |
| 1063 | return; |
| 1064 | } |
| 1065 | if (!cbm_workspace_root_allowed(canonical_root, cbm_workspace_home_dir(), |
| 1066 | cbm_workspace_cache_dir(), getenv("CBM_ALLOWED_ROOT"), |
| 1067 | boundary_err, sizeof(boundary_err))) { |
| 1068 | yyjson_doc_free(doc); |
| 1069 | char escaped[1024]; |
| 1070 | cbm_json_escape(escaped, (int)sizeof(escaped), boundary_err); |
| 1071 | cbm_http_replyf(c, 403, g_cors_json, "{\"error\":\"%s\"}", escaped); |
| 1072 | return; |
| 1073 | } |
| 1074 | |
| 1075 | /* Find free job slot */ |
no test coverage detected